Compare commits
66 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
1019fecc9e | ||
![]() |
1855f93c1c | ||
![]() |
52e6a44aa4 | ||
![]() |
0b77808af6 | ||
![]() |
9233ed5c53 | ||
![]() |
ee68c0f622 | ||
![]() |
366823cee9 | ||
![]() |
40e1eb9a49 | ||
![]() |
1af419a860 | ||
![]() |
397f18c435 | ||
![]() |
2e5dd05a6c | ||
![]() |
a9fb8ddfb8 | ||
![]() |
562c726787 | ||
![]() |
5f82c1dc17 | ||
![]() |
222800bdb6 | ||
![]() |
5dd3636571 | ||
![]() |
2296a9fbb3 | ||
![]() |
63b5a7c036 | ||
![]() |
b74ca2670e | ||
![]() |
393f4e0e58 | ||
![]() |
3a9ca29e99 | ||
![]() |
32995fef24 | ||
![]() |
a73c99fc64 | ||
![]() |
a5834470ba | ||
![]() |
da3bc127dc | ||
![]() |
0dddc4d58f | ||
![]() |
a4d5d9157b | ||
![]() |
c70d5d4398 | ||
![]() |
7c08b07ef5 | ||
![]() |
e426b5dd35 | ||
![]() |
2fdf619582 | ||
![]() |
d9eed14b7a | ||
![]() |
8230ffb8a4 | ||
![]() |
7098930b19 | ||
![]() |
56244245a4 | ||
![]() |
dd2f12fa8e | ||
![]() |
9598247a0d | ||
![]() |
230ee90b1c | ||
![]() |
e705bedc91 | ||
![]() |
b5ebe7590c | ||
![]() |
6d0831ceaa | ||
![]() |
19e00ee2f2 | ||
![]() |
80723d224e | ||
![]() |
0c82bb023a | ||
![]() |
0a86f24095 | ||
![]() |
b41249cfa8 | ||
![]() |
6659802689 | ||
![]() |
964c503223 | ||
![]() |
15568bf20a | ||
![]() |
d10cd324bb | ||
![]() |
2a22ab8c33 | ||
![]() |
ca736cdae2 | ||
![]() |
d589c57dd2 | ||
![]() |
9b0caf2a47 | ||
![]() |
f8b00bbd67 | ||
![]() |
91a8c0e7a0 | ||
![]() |
2089172384 | ||
![]() |
1ab87e5334 | ||
![]() |
b5e6861032 | ||
![]() |
189930918a | ||
![]() |
ff1bd0a4b8 | ||
![]() |
e544d0dd07 | ||
![]() |
3e0b240154 | ||
![]() |
199119cafb | ||
![]() |
89ab665923 | ||
![]() |
dfb60de6d2 |
@@ -5,6 +5,7 @@ contrib
|
||||
init-scripts
|
||||
package
|
||||
pylintrc
|
||||
snap
|
||||
*.md
|
||||
!CHANGELOG*.md
|
||||
start.bat
|
||||
|
20
.github/pull_request_template.md
vendored
Normal file
20
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
## Description
|
||||
|
||||
Please include a summary of the change and which issue is fixed.
|
||||
|
||||
Fixes Tautulli/Tautulli-Issues#(issue)
|
||||
|
||||
## Type of change
|
||||
|
||||
Please delete options that are not relevant.
|
||||
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
|
||||
## Checklist:
|
||||
|
||||
- [ ] My code follows the style guidelines of this project
|
||||
- [ ] I have performed a self-review of my own code
|
||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||
- [ ] I have added or updated the docstring for new or existing methods
|
46
.github/workflows/publish-docker.yml
vendored
46
.github/workflows/publish-docker.yml
vendored
@@ -1,10 +1,14 @@
|
||||
name: Publish Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, beta, nightly, python3]
|
||||
branches: [master, beta, nightly]
|
||||
tags: [v*]
|
||||
pull_request: ~
|
||||
|
||||
jobs:
|
||||
build:
|
||||
build-docker:
|
||||
name: Build Docker Image
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
@@ -20,7 +24,9 @@ jobs:
|
||||
else
|
||||
echo ::set-output name=tag::${GITHUB_REF#refs/heads/}
|
||||
fi
|
||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
if [[ $GITHUB_REF == refs/tags/*-beta ]]; then
|
||||
echo ::set-output name=branch::beta
|
||||
elif [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
echo ::set-output name=branch::master
|
||||
else
|
||||
echo ::set-output name=branch::${GITHUB_REF#refs/heads/}
|
||||
@@ -30,14 +36,12 @@ jobs:
|
||||
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
|
||||
|
||||
- name: Set up QEMU
|
||||
- name: Set Up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
with:
|
||||
platforms: all
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
id: buildx
|
||||
with:
|
||||
version: latest
|
||||
|
||||
@@ -51,14 +55,14 @@ jobs:
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
if: success()
|
||||
if: success() && github.event_name != 'pull_request'
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v1
|
||||
if: success()
|
||||
if: success() && github.event_name != 'pull_request'
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
@@ -70,7 +74,7 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
platforms: ${{ steps.prepare.outputs.docker_platforms }}
|
||||
build-args: |
|
||||
TAG=${{ steps.prepare.outputs.tag }}
|
||||
@@ -83,11 +87,29 @@ jobs:
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||
|
||||
discord:
|
||||
name: Discord Notification
|
||||
needs: build-docker
|
||||
if: always() && github.event_name != 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get Build Job Status
|
||||
uses: technote-space/workflow-conclusion-action@v1
|
||||
|
||||
- name: Combine Job Status
|
||||
id: status
|
||||
run: |
|
||||
failures=(neutral, skipped, timed_out, action_required)
|
||||
if [[ ${array[@]} =~ $WORKFLOW_CONCLUSION ]]; then
|
||||
echo ::set-output name=status::failure
|
||||
else
|
||||
echo ::set-output name=status::$WORKFLOW_CONCLUSION
|
||||
fi
|
||||
|
||||
- name: Post Status to Discord
|
||||
uses: sarisia/actions-status-discord@v1
|
||||
if: always()
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
status: ${{ job.status }}
|
||||
status: ${{ steps.status.outputs.status }}
|
||||
title: ${{ github.workflow }}
|
||||
nofail: true
|
||||
|
@@ -1,12 +1,26 @@
|
||||
name: Publish Release
|
||||
name: Publish Installers
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, beta, nightly, python3]
|
||||
branches: [master, beta, nightly]
|
||||
tags: [v*]
|
||||
pull_request: ~
|
||||
|
||||
jobs:
|
||||
build-windows:
|
||||
runs-on: windows-latest
|
||||
build-installer:
|
||||
name: Build ${{ matrix.os_upper }} Installer
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: 'windows'
|
||||
os_upper: 'Windows'
|
||||
ext: 'exe'
|
||||
- os: 'macos'
|
||||
os_upper: 'MacOS'
|
||||
ext: 'pkg'
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
@@ -16,11 +30,13 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||
VERSION_NSIS=${GITHUB_REF#refs/tags/v}.1
|
||||
echo ::set-output name=VERSION_NSIS::${VERSION_NSIS/%-beta.1/.0}
|
||||
echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/v}
|
||||
echo ::set-output name=RELEASE_VERSION::${GITHUB_REF#refs/tags/}
|
||||
else
|
||||
echo "VERSION=0.0.0" >> $GITHUB_ENV
|
||||
echo ::set-output name=VERSION_NSIS::0.0.0.0
|
||||
echo ::set-output name=VERSION::0.0.0
|
||||
echo ::set-output name=RELEASE_VERSION::${GITHUB_SHA::7}
|
||||
@@ -33,110 +49,52 @@ jobs:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Cache Dependencies
|
||||
id: cache_dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~\AppData\Local\pip\Cache
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('package/requirements-windows.txt') }}
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles(format('package/requirements-{0}.txt', matrix.os)) }}
|
||||
restore-keys: ${{ runner.os }}-pip-
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r package/requirements-windows.txt
|
||||
pip install -r package/requirements-${{ matrix.os }}.txt
|
||||
|
||||
- name: Build Package
|
||||
run: |
|
||||
pyinstaller -y ./package/Tautulli-windows.spec
|
||||
pyinstaller -y ./package/Tautulli-${{ matrix.os }}.spec
|
||||
|
||||
- name: Create Installer
|
||||
- name: Create Windows Installer
|
||||
uses: joncloud/makensis-action@v1.2
|
||||
if: matrix.os == 'windows'
|
||||
with:
|
||||
script-file: ./package/Tautulli.nsi
|
||||
arguments: /DVERSION=${{ steps.get_version.outputs.VERSION_NSIS }} /DINSTALLER_NAME=..\Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.exe
|
||||
arguments: >
|
||||
/DVERSION=${{ steps.get_version.outputs.VERSION_NSIS }}
|
||||
/DINSTALLER_NAME=..\Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.exe
|
||||
include-more-plugins: true
|
||||
include-custom-plugins-path: package/nsis-plugins
|
||||
|
||||
- name: Upload Installer
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Tautulli-windows-installer
|
||||
path: Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.exe
|
||||
|
||||
- name: Post Status to Discord
|
||||
uses: sarisia/actions-status-discord@v1
|
||||
if: always()
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
status: ${{ job.status }}
|
||||
title: Build Windows Installer
|
||||
nofail: true
|
||||
|
||||
build-macos:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set Release Version
|
||||
id: get_version
|
||||
shell: bash
|
||||
- name: Create MacOS Installer
|
||||
if: matrix.os == 'macos'
|
||||
run: |
|
||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||
echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/v}
|
||||
echo ::set-output name=RELEASE_VERSION::${GITHUB_REF#refs/tags/}
|
||||
else
|
||||
echo "VERSION=0.0.0" >> $GITHUB_ENV
|
||||
echo ::set-output name=VERSION::0.0.0
|
||||
echo ::set-output name=RELEASE_VERSION::${GITHUB_SHA::7}
|
||||
fi
|
||||
echo $GITHUB_SHA > version.txt
|
||||
|
||||
- name: Set Up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Cache Dependencies
|
||||
id: cache_dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/Library/Caches/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('package/requirements-macos.txt') }}
|
||||
restore-keys: ${{ runner.os }}-pip-
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r package/requirements-macos.txt
|
||||
|
||||
- name: Build Package
|
||||
run: |
|
||||
pyinstaller -y ./package/Tautulli-macos.spec
|
||||
|
||||
- name: Create Installer
|
||||
run: |
|
||||
sudo pkgbuild --install-location /Applications --version ${{ steps.get_version.outputs.VERSION }} --component ./dist/Tautulli.app --scripts ./package/macos-scripts Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.pkg
|
||||
sudo pkgbuild \
|
||||
--install-location /Applications \
|
||||
--version ${{ steps.get_version.outputs.VERSION }} \
|
||||
--component ./dist/Tautulli.app \
|
||||
--scripts ./package/macos-scripts \
|
||||
Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.pkg
|
||||
|
||||
- name: Upload Installer
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Tautulli-macos-installer
|
||||
path: Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.pkg
|
||||
|
||||
- name: Post Status to Discord
|
||||
uses: sarisia/actions-status-discord@v1
|
||||
if: always()
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
status: ${{ job.status }}
|
||||
title: Build MacOS Installer
|
||||
nofail: true
|
||||
name: Tautulli-${{ matrix.os }}-installer
|
||||
path: Tautulli-${{ matrix.os }}-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.${{ matrix.ext }}
|
||||
|
||||
release:
|
||||
needs: [build-windows, build-macos]
|
||||
if: startsWith(github.ref, 'refs/tags/') && always()
|
||||
name: Release Installers
|
||||
needs: build-installer
|
||||
if: always() && startsWith(github.ref, 'refs/tags/') && github.event_name != 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get Build Job Status
|
||||
@@ -150,25 +108,19 @@ jobs:
|
||||
run: |
|
||||
echo ::set-output name=RELEASE_VERSION::${GITHUB_REF#refs/tags/}
|
||||
|
||||
- name: Download Windows Installer
|
||||
- name: Download Installers
|
||||
if: env.WORKFLOW_CONCLUSION == 'success'
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: Tautulli-windows-installer
|
||||
|
||||
- name: Download MacOS Installer
|
||||
if: env.WORKFLOW_CONCLUSION == 'success'
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: Tautulli-macos-installer
|
||||
|
||||
- name: Get Changelog
|
||||
id: get_changelog
|
||||
run: echo ::set-output name=CHANGELOG::"$( sed -n '/^## /{p; :loop n; p; /^## /q; b loop}' CHANGELOG.md | sed '$d' | sed '$d' | sed '$d' | sed ':a;N;$!ba;s/\n/%0A/g' )"
|
||||
run: |
|
||||
echo ::set-output name=CHANGELOG::"$( sed -n '/^## /{p; :loop n; p; /^## /q; b loop}' CHANGELOG.md \
|
||||
| sed '$d' | sed '$d' | sed '$d' | sed ':a;N;$!ba;s/\n/%0A/g' )"
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
id: create_release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
@@ -182,23 +134,50 @@ jobs:
|
||||
prerelease: ${{ endsWith(steps.get_version.outputs.RELEASE_VERSION, '-beta') }}
|
||||
|
||||
- name: Upload Windows Installer
|
||||
if: env.WORKFLOW_CONCLUSION == 'success'
|
||||
uses: actions/upload-release-asset@v1
|
||||
if: env.WORKFLOW_CONCLUSION == 'success'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.exe
|
||||
asset_path: Tautulli-windows-installer/Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.exe
|
||||
asset_name: Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.exe
|
||||
asset_content_type: application/vnd.microsoft.portable-executable
|
||||
|
||||
- name: Upload MacOS Installer
|
||||
if: env.WORKFLOW_CONCLUSION == 'success'
|
||||
uses: actions/upload-release-asset@v1
|
||||
if: env.WORKFLOW_CONCLUSION == 'success'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.pkg
|
||||
asset_path: Tautulli-macos-installer/Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.pkg
|
||||
asset_name: Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.pkg
|
||||
asset_content_type: application/vnd.apple.installer+xml
|
||||
|
||||
discord:
|
||||
name: Discord Notification
|
||||
needs: [build-installer, release]
|
||||
if: always() && github.event_name != 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get Build Job Status
|
||||
uses: technote-space/workflow-conclusion-action@v1
|
||||
|
||||
- name: Combine Job Status
|
||||
id: status
|
||||
run: |
|
||||
failures=(neutral, skipped, timed_out, action_required)
|
||||
if [[ ${array[@]} =~ $WORKFLOW_CONCLUSION ]]; then
|
||||
echo ::set-output name=status::failure
|
||||
else
|
||||
echo ::set-output name=status::$WORKFLOW_CONCLUSION
|
||||
fi
|
||||
|
||||
- name: Post Status to Discord
|
||||
uses: sarisia/actions-status-discord@v1
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
status: ${{ steps.status.outputs.status }}
|
||||
title: ${{ github.workflow }}
|
||||
nofail: true
|
94
.github/workflows/publish-snap.yml
vendored
Normal file
94
.github/workflows/publish-snap.yml
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
name: Publish Snap
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, beta, nightly]
|
||||
tags: [v*]
|
||||
pull_request: ~
|
||||
|
||||
jobs:
|
||||
build-snap:
|
||||
name: Build Snap Package (${{ matrix.architecture }})
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
architecture:
|
||||
- i386
|
||||
- amd64
|
||||
- arm64
|
||||
- armhf
|
||||
- ppc64el
|
||||
#- s390x # broken at the moment
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Prepare
|
||||
id: prepare
|
||||
run: |
|
||||
git fetch --prune --unshallow --tags
|
||||
if [[ $GITHUB_REF == refs/tags/*-beta || $GITHUB_REF == refs/heads/beta ]]; then
|
||||
echo ::set-output name=RELEASE::beta
|
||||
elif [[ $GITHUB_REF == refs/tags/* || $GITHUB_REF == refs/heads/master ]]; then
|
||||
echo ::set-output name=RELEASE::stable
|
||||
else
|
||||
echo ::set-output name=RELEASE::edge
|
||||
fi
|
||||
|
||||
- name: Set Up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
- name: Build Snap Package
|
||||
uses: diddlesnaps/snapcraft-multiarch-action@v1
|
||||
id: build
|
||||
with:
|
||||
architecture: ${{ matrix.architecture }}
|
||||
|
||||
- name: Upload Snap Package
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Tautulli-snap-package-${{ matrix.architecture }}
|
||||
path: ${{ steps.build.outputs.snap }}
|
||||
|
||||
- name: Review Snap Package
|
||||
uses: diddlesnaps/snapcraft-review-tools-action@v1
|
||||
with:
|
||||
snap: ${{ steps.build.outputs.snap }}
|
||||
|
||||
- name: Publish Snap Package
|
||||
uses: snapcore/action-publish@v1
|
||||
if: >
|
||||
github.event_name != 'pull_request' &&
|
||||
(startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/nightly')
|
||||
with:
|
||||
store_login: ${{ secrets.SNAP_LOGIN }}
|
||||
snap: ${{ steps.build.outputs.snap }}
|
||||
release: ${{ steps.prepare.outputs.RELEASE }}
|
||||
|
||||
discord:
|
||||
name: Discord Notification
|
||||
needs: build-snap
|
||||
if: always() && github.event_name != 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get Build Job Status
|
||||
uses: technote-space/workflow-conclusion-action@v1
|
||||
|
||||
- name: Combine Job Status
|
||||
id: status
|
||||
run: |
|
||||
failures=(neutral, skipped, timed_out, action_required)
|
||||
if [[ ${array[@]} =~ $WORKFLOW_CONCLUSION ]]; then
|
||||
echo ::set-output name=status::failure
|
||||
else
|
||||
echo ::set-output name=status::$WORKFLOW_CONCLUSION
|
||||
fi
|
||||
|
||||
- name: Post Status to Discord
|
||||
uses: sarisia/actions-status-discord@v1
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
status: ${{ steps.status.outputs.status }}
|
||||
title: ${{ github.workflow }}
|
||||
nofail: true
|
13
.gitignore
vendored
13
.gitignore
vendored
@@ -81,3 +81,16 @@ _ReSharper*/
|
||||
#Ignore files generated by pyinstaller
|
||||
/build
|
||||
/dist
|
||||
|
||||
#snapcraft specifics
|
||||
/parts/
|
||||
/stage/
|
||||
/prime/
|
||||
|
||||
*.snap
|
||||
|
||||
.snapcraft
|
||||
__pycache__
|
||||
*.pyc
|
||||
*_source.tar.bz2
|
||||
snap/.snapcraft
|
35
CHANGELOG.md
35
CHANGELOG.md
@@ -1,5 +1,40 @@
|
||||
# Changelog
|
||||
|
||||
## v2.6.4 (2020-12-20)
|
||||
|
||||
* Other:
|
||||
* Fix: Restore Snap data folder from previous installs.
|
||||
|
||||
|
||||
## v2.6.3 (2020-12-19)
|
||||
|
||||
* Announcements:
|
||||
* This is the last Tautulli version to support Python 2. Python 3 will be required to continue receiving updates. You can check your Python version on the settings page.
|
||||
* Exporter:
|
||||
* Fix: Accessible and exists attributes were blank for media info export level 9.
|
||||
* UI:
|
||||
* Fix: Guest usernames were not masked on mouse hover.
|
||||
* Other:
|
||||
* Fix: macOS menu bar icon for light and dark mode.
|
||||
* New: Tautulli can officially be installed on Linux using a Snap package. See the installation wiki for details.
|
||||
|
||||
|
||||
## v2.6.2 (2020-12-05)
|
||||
|
||||
* Notifications:
|
||||
* Change: Send a notification of a user new device for the first time only. This can be toggled off in the settings.
|
||||
* Exporter:
|
||||
* Fix: Allow exporting child fields only without requiring the parent fields as well.
|
||||
* Fix: Exporting individual collection would fail.
|
||||
* Change: Remove accessible and exists fields from the default media info export levels. This prevents the Plex server from reading the media files unnecessarily.
|
||||
* Other:
|
||||
* Fix: Enable high resolution for the macOS system tray icon and menu.
|
||||
* New: Added rate limiting for failed login attempts.
|
||||
* Change: Use a white logo for the macOS system tray icon.
|
||||
* API:
|
||||
* New: Added machine_id to the get_history API response.
|
||||
|
||||
|
||||
## v2.6.1 (2020-11-03)
|
||||
|
||||
* Other:
|
||||
|
@@ -38,7 +38,8 @@ This project is based on code from [Headphones](https://github.com/rembo10/headp
|
||||
| --- | --- | --- | --- |
|
||||
| Release | [](https://github.com/Tautulli/Tautulli/releases/latest) <br> [](https://github.com/Tautulli/Tautulli/releases/latest) | [](https://github.com/Tautulli/Tautulli/releases) <br> [](https://github.com/Tautulli/Tautulli/commits/beta) | [](https://github.com/Tautulli/Tautulli/commits/nightly) <br> [](https://github.com/Tautulli/Tautulli/commits/nightly) |
|
||||
| Docker | [](https://hub.docker.com/r/tautulli/tautulli) <br> [](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Docker"+branch%3Amaster) | [](https://hub.docker.com/r/tautulli/tautulli) <br> [](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Docker"+branch%3Abeta) | [](https://hub.docker.com/r/tautulli/tautulli) <br> [](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Docker"+branch%3Anightly) |
|
||||
| Installer | [](https://github.com/Tautulli/Tautulli/releases/latest) <br> [](https://github.com/Tautulli/Tautulli/releases/latest) <br> [](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Release"+branch%3Amaster) | [](https://github.com/Tautulli/Tautulli/releases) <br> [](https://github.com/Tautulli/Tautulli/releases) <br> [](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Release"+branch%3Abeta) | [](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Release"+branch%3Anightly) |
|
||||
| Snap | [](https://snapcraft.io/tautulli) <br> [](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Snap"+branch%3Amaster) | [](https://snapcraft.io/tautulli) <br> [](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Snap"+branch%3Abeta) | [](https://snapcraft.io/tautulli) <br> [](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Snap"+branch%3Anightly) |
|
||||
| Installer | [](https://github.com/Tautulli/Tautulli/releases/latest) <br> [](https://github.com/Tautulli/Tautulli/releases/latest) <br> [](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Installers"+branch%3Amaster) | [](https://github.com/Tautulli/Tautulli/releases) <br> [](https://github.com/Tautulli/Tautulli/releases) <br> [](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Installers"+branch%3Abeta) | [](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Installers"+branch%3Anightly) |
|
||||
|
||||
[](https://github.com/Tautulli/Tautulli-Wiki/wiki)
|
||||
[](https://tautulli.com/discord)
|
||||
|
@@ -124,6 +124,8 @@ def main():
|
||||
|
||||
if helpers.bool_true(os.getenv('TAUTULLI_DOCKER', False)):
|
||||
plexpy.DOCKER = True
|
||||
if helpers.bool_true(os.getenv('TAUTULLI_SNAP', False)):
|
||||
plexpy.SNAP = True
|
||||
|
||||
if args.dev:
|
||||
plexpy.DEV = True
|
||||
|
@@ -59,6 +59,8 @@
|
||||
% endif
|
||||
% if plexpy.INSTALL_TYPE == 'docker':
|
||||
Update your Docker container or <a href="#" id="updateDismiss">Dismiss</a>
|
||||
% elif plexpy.INSTALL_TYPE == 'snap':
|
||||
Update your Snap package or <a href="#" id="updateDismiss">Dismiss</a>
|
||||
% elif plexpy.INSTALL_TYPE 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" rel="noreferrer">Download</a> and install the latest version or <a href="#" id="updateDismiss">Dismiss</a>
|
||||
% else:
|
||||
@@ -204,7 +206,7 @@ ${next.modalIncludes()}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<span id="incorrect-login" style="padding-right: 25px; display: none;">Incorrect username or password.</span>
|
||||
<span id="sign-in-alert" style="padding-right: 25px; display: none;"></span>
|
||||
<button id="sign-in" type="submit" class="btn btn-bright login-button"><i class="fa fa-sign-in"></i> Sign In</button>
|
||||
</div>
|
||||
<input type="hidden" id="admin_login" name="admin_login" value="1" />
|
||||
@@ -337,6 +339,8 @@ ${next.modalIncludes()}
|
||||
}
|
||||
if (result.install_type === 'docker') {
|
||||
msg += 'Update your Docker container or <a href="#" id="updateDismiss">Dismiss</a>';
|
||||
} else if (result.install_type === 'snap') {
|
||||
msg += 'Update your Snap package or <a href="#" id="updateDismiss">Dismiss</a>';
|
||||
} else if (result.install_type === 'windows' || result.install_type === 'macos') {
|
||||
msg += '<a href="' + result.release_url + '" target="_blank" rel="noreferrer">Download</a> and install the latest version or <a href="#" id="updateDismiss">Dismiss</a>'
|
||||
} else {
|
||||
@@ -446,12 +450,16 @@ ${next.modalIncludes()}
|
||||
data: $(this).serialize(),
|
||||
dataType: 'json',
|
||||
statusCode: {
|
||||
200: function() {
|
||||
200: function(xhr, status) {
|
||||
window.location = "${http_root}";
|
||||
},
|
||||
401: function() {
|
||||
$('#incorrect-login').show();
|
||||
$('#username').focus();
|
||||
401: function(xhr, status) {
|
||||
$('#sign-in-alert').text('Incorrect username or password.').show();
|
||||
$('#username').focus();
|
||||
},
|
||||
429: function(xhr, status) {
|
||||
var retry = Math.ceil(xhr.getResponseHeader('Retry-After') / 60)
|
||||
$('#sign-in-alert').text('Too many login attempts. Try again in ' + retry + ' minute(s).').show();
|
||||
}
|
||||
},
|
||||
complete: function() {
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 123 KiB |
BIN
data/interfaces/default/images/logo-flat-white.ico
Normal file
BIN
data/interfaces/default/images/logo-flat-white.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 200 KiB |
BIN
data/interfaces/default/images/logo-flat-white.png
Normal file
BIN
data/interfaces/default/images/logo-flat-white.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
@@ -212,6 +212,28 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% from plexpy.helpers import anon_url %>
|
||||
<div id="python2-modal" class="modal fade wide" tabindex="-1" role="dialog" aria-labelledby="python2-modal">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
|
||||
<h4 class="modal-title">Unable to Update</h4>
|
||||
</div>
|
||||
<div class="modal-body" style="text-align: center;">
|
||||
<p>Tautulli is still running using Python 2 and cannot be updated past v2.6.3.</p>
|
||||
<p>Python 3 is required to continue receiving updates.</p>
|
||||
<p>
|
||||
<strong>Please see the <a href="${anon_url('https://github.com/Tautulli/Tautulli-Wiki/wiki/Upgrading-to-Python-3-%28Tautulli-v2.5%29')}" target="_blank" rel="noreferrer">wiki</a>
|
||||
for instructions on how to upgrade to Python 3.</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<input type="button" class="btn btn-bright" data-dismiss="modal" value="Close">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
<div class="modal fade" id="ip-info-modal" tabindex="-1" role="dialog" aria-labelledby="ip-info-modal">
|
||||
@@ -1010,4 +1032,16 @@
|
||||
});
|
||||
</script>
|
||||
% endif
|
||||
% if _session['user_group'] == 'admin':
|
||||
<script>
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
if (urlParams.get('update') === 'python2') {
|
||||
$("#python2-modal").modal({
|
||||
backdrop: 'static',
|
||||
keyboard: false
|
||||
});
|
||||
}
|
||||
</script>
|
||||
% endif
|
||||
</%def>
|
@@ -159,16 +159,20 @@
|
||||
data: data,
|
||||
dataType: 'json',
|
||||
statusCode: {
|
||||
200: function() {
|
||||
200: function(xhr, status) {
|
||||
window.location = "${redirect_uri or http_root}";
|
||||
},
|
||||
401: function() {
|
||||
401: function(xhr, status) {
|
||||
if (plex) {
|
||||
$('#sign-in-alert').text('Invalid Plex Login.').show();
|
||||
} else {
|
||||
$('#sign-in-alert').text('Incorrect username or password.').show();
|
||||
$('#username').focus();
|
||||
}
|
||||
},
|
||||
429: function(xhr, status) {
|
||||
var retry = Math.ceil(xhr.getResponseHeader('Retry-After') / 60)
|
||||
$('#sign-in-alert').text('Too many login attempts. Try again in ' + retry + ' minute(s).').show();
|
||||
}
|
||||
},
|
||||
complete: function() {
|
||||
|
@@ -220,7 +220,7 @@
|
||||
<p class="help-block">Check for Tautulli updates periodically.</p>
|
||||
</div>
|
||||
<div id="git_update_options">
|
||||
% if not plexpy.FROZEN:
|
||||
% if not plexpy.SNAP and not plexpy.FROZEN:
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="plexpy_auto_update" name="plexpy_auto_update" value="1" ${config['plexpy_auto_update']} ${docker_setting}> Update Automatically ${docker_msg | n}
|
||||
@@ -977,15 +977,21 @@
|
||||
</div>
|
||||
<div class="checkbox advanced-setting">
|
||||
<label>
|
||||
<input type="checkbox" name="notify_consecutive" id="notify_consecutive" value="1" ${config['notify_consecutive']}> Allow Consecutive Notifications
|
||||
<input type="checkbox" name="notify_consecutive" id="notify_consecutive" value="1" ${config['notify_consecutive']}> Allow Playback Stop Notifications Exceeding Watched Percent
|
||||
</label>
|
||||
<p class="help-block">Enable to allow sending of consecutive notifications (i.e. both watched & stopped notifications).</p>
|
||||
<p class="help-block">
|
||||
Enable to allow sending of playback stop notifications after the watched percent is exceeded.
|
||||
Disable to only send playback stop notifications below the watched percent.
|
||||
</p>
|
||||
</div>
|
||||
<div class="checkbox advanced-setting">
|
||||
<label>
|
||||
<input type="checkbox" name="notify_concurrent_by_ip" id="notify_concurrent_by_ip" value="1" ${config['notify_concurrent_by_ip']}> User Concurrent Streams Notifications by IP Address
|
||||
</label>
|
||||
<p class="help-block">Enable to only send a notification of concurrent streams by a single user from different IP addresses.</p>
|
||||
<p class="help-block">
|
||||
Enable to only send a concurrent streams notification by a single user from different IP addresses.
|
||||
Disable to send a concurrent streams notification anytime the concurrent stream threshold is exceeded regardless of IP address.
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="notify_concurrent_threshold">User Concurrent Stream Threshold</label>
|
||||
@@ -997,6 +1003,15 @@
|
||||
</div>
|
||||
<p class="help-block">The number of concurrent streams by a single user for Tautulli to trigger a notification. Minimum 2.</p>
|
||||
</div>
|
||||
<div class="checkbox advanced-setting">
|
||||
<label>
|
||||
<input type="checkbox" name="notify_new_device_initial_only" id="notify_new_device_initial_only" value="1" ${config['notify_new_device_initial_only']}> User New Device Notification First Time Only
|
||||
</label>
|
||||
<p class="help-block">
|
||||
Enable to only send a new device notification the first time a user streams from a new device.
|
||||
Disable to send a new device notification everytime a user streams from the device until it is recorded in history (i.e. exceeds the ignore interval).
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group advanced-setting">
|
||||
<label for="notify_concurrent_threshold">Continued Session Threshold</label>
|
||||
<div class="row">
|
||||
@@ -2911,10 +2926,6 @@ $(document).ready(function() {
|
||||
});
|
||||
});
|
||||
|
||||
$('#http_base_url').change(function () {
|
||||
$(this).val($(this).val().replace(/\/*$/, ''));
|
||||
});
|
||||
|
||||
$('#http_root').change(function() {
|
||||
setBaseURLSuffix();
|
||||
});
|
||||
|
@@ -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
|
||||
print(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())
|
||||
|
@@ -58,38 +58,6 @@ AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7
|
||||
TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg==
|
||||
-----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
|
||||
# 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"
|
||||
@@ -152,39 +120,6 @@ ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS
|
||||
R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp
|
||||
-----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.
|
||||
# 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"
|
||||
@@ -640,46 +575,6 @@ VSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY
|
||||
WQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q=
|
||||
-----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
|
||||
# Subject: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com
|
||||
# Label: "DigiCert Assured ID Root CA"
|
||||
@@ -1127,38 +1022,6 @@ fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv
|
||||
GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY=
|
||||
-----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
|
||||
# Subject: CN=Certigna O=Dhimyotis
|
||||
# Label: "Certigna"
|
||||
@@ -1499,47 +1362,6 @@ uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2
|
||||
XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E=
|
||||
-----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-----
|
||||
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
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
# Issuer: 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"
|
||||
@@ -2140,6 +1962,45 @@ t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy
|
||||
SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03
|
||||
-----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
|
||||
# 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"
|
||||
@@ -2352,38 +2213,6 @@ e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p
|
||||
TpPDpFQUWw==
|
||||
-----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
|
||||
# Subject: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH
|
||||
# Label: "D-TRUST Root Class 3 CA 2 2009"
|
||||
@@ -3749,47 +3578,6 @@ CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW
|
||||
1KyLa2tJElMzrdfkviT8tQp21KW8EA==
|
||||
-----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-----
|
||||
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
|
||||
-----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
|
||||
# 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"
|
||||
@@ -4600,3 +4388,219 @@ IQ6SwJAfzyBfyjs4x7dtOvPmRLgOMWuIjnDrnBdSqEGULoe256YSxXXfW8AKbnuk
|
||||
5F6G+TaU33fD6Q3AOfF5u0aOq0NZJ7cguyPpVkAh7DE9ZapD8j3fcEThuk0mEDuY
|
||||
n/PIjhs4ViFqUZPTkcpG2om3PVODLAgfi49T3f+sHw==
|
||||
-----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-----
|
||||
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
|
||||
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-----
|
||||
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
|
||||
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
|
||||
~~~~~~~~~~
|
||||
|
||||
This module returns the installation location of cacert.pem.
|
||||
This module returns the installation location of cacert.pem or its contents.
|
||||
"""
|
||||
import os
|
||||
|
||||
try:
|
||||
from importlib.resources import path as get_path, read_text
|
||||
|
||||
def where():
|
||||
f = os.path.dirname(__file__)
|
||||
_CACERT_CTX = None
|
||||
_CACERT_PATH = None
|
||||
|
||||
return os.path.join(f, 'cacert.pem')
|
||||
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():
|
||||
f = os.path.dirname(__file__)
|
||||
|
||||
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):
|
||||
""" 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.
|
||||
"""
|
||||
log.info('Stopping AlertListener.')
|
||||
|
@@ -10,6 +10,8 @@ class Audio(PlexPartialObject):
|
||||
|
||||
Attributes:
|
||||
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).
|
||||
key (str): API URL (/library/metadata/<ratingkey>).
|
||||
lastViewedAt (datetime): Datetime item was last accessed.
|
||||
@@ -18,6 +20,7 @@ class Audio(PlexPartialObject):
|
||||
ratingKey (int): Unique key identifying this item.
|
||||
summary (str): Summary of the artist, track, or album.
|
||||
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.)
|
||||
titleSort (str): Title to use when sorting (defaults to title).
|
||||
type (str): 'artist', 'album', or 'track'.
|
||||
@@ -32,6 +35,8 @@ class Audio(PlexPartialObject):
|
||||
self._data = data
|
||||
self.listType = 'audio'
|
||||
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.key = data.attrib.get('key')
|
||||
self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt'))
|
||||
@@ -41,6 +46,7 @@ class Audio(PlexPartialObject):
|
||||
self.ratingKey = utils.cast(int, data.attrib.get('ratingKey'))
|
||||
self.summary = data.attrib.get('summary')
|
||||
self.thumb = data.attrib.get('thumb')
|
||||
self.thumbBlurHash = data.attrib.get('thumbBlurHash')
|
||||
self.title = data.attrib.get('title')
|
||||
self.titleSort = data.attrib.get('titleSort', self.title)
|
||||
self.type = data.attrib.get('type')
|
||||
@@ -69,20 +75,20 @@ class Audio(PlexPartialObject):
|
||||
|
||||
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.
|
||||
See :func:`plexapi.myplex.MyPlexAccount.sync()` for possible exceptions.
|
||||
See :func:`~plexapi.myplex.MyPlexAccount.sync` for possible exceptions.
|
||||
|
||||
Parameters:
|
||||
bitrate (int): maximum bitrate for synchronized music, better use one of MUSIC_BITRATE_* values from the
|
||||
module :mod:`plexapi.sync`.
|
||||
client (:class:`plexapi.myplex.MyPlexDevice`): sync destination, see
|
||||
:func:`plexapi.myplex.MyPlexAccount.sync`.
|
||||
clientId (str): sync destination, see :func:`plexapi.myplex.MyPlexAccount.sync`.
|
||||
module :mod:`~plexapi.sync`.
|
||||
client (:class:`~plexapi.myplex.MyPlexDevice`): 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`.
|
||||
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.
|
||||
|
||||
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
|
||||
@@ -111,7 +117,6 @@ class Artist(Audio):
|
||||
Attributes:
|
||||
TAG (str): 'Directory'
|
||||
TYPE (str): 'artist'
|
||||
art (str): Artist artwork (/library/metadata/<ratingkey>/art/<artid>)
|
||||
countries (list): List of :class:`~plexapi.media.Country` 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)
|
||||
@@ -122,17 +127,10 @@ class Artist(Audio):
|
||||
TAG = 'Directory'
|
||||
TYPE = 'artist'
|
||||
|
||||
_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):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
Audio._loadData(self, data)
|
||||
self.key = self.key.replace('/children', '') # FIX_BUG_50
|
||||
self._details_key = self.key + self._include
|
||||
self.art = data.attrib.get('art')
|
||||
self.guid = data.attrib.get('guid')
|
||||
self.locations = self.listAttrs(data, 'path', etag='Location')
|
||||
self.countries = self.findItems(data, media.Country)
|
||||
@@ -187,7 +185,7 @@ class Artist(Audio):
|
||||
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
|
||||
"<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
|
||||
function. If kwargs is not specified, the media items will be downloaded
|
||||
and saved to disk.
|
||||
@@ -206,7 +204,6 @@ class Album(Audio):
|
||||
Attributes:
|
||||
TAG (str): 'Directory'
|
||||
TYPE (str): 'album'
|
||||
art (str): Album artwork (/library/metadata/<ratingkey>/art/<artid>)
|
||||
genres (list): List of :class:`~plexapi.media.Genre` objects this album respresents.
|
||||
key (str): API URL (/library/metadata/<ratingkey>).
|
||||
originallyAvailableAt (datetime): Datetime this album was released.
|
||||
@@ -227,7 +224,6 @@ class Album(Audio):
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
Audio._loadData(self, data)
|
||||
self.art = data.attrib.get('art')
|
||||
self.guid = data.attrib.get('guid')
|
||||
self.leafCount = utils.cast(int, data.attrib.get('leafCount'))
|
||||
self.loudnessAnalysisVersion = utils.cast(int, data.attrib.get('loudnessAnalysisVersion'))
|
||||
@@ -279,7 +275,7 @@ class Album(Audio):
|
||||
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
|
||||
"<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
|
||||
function. If kwargs is not specified, the media items will be downloaded
|
||||
and saved to disk.
|
||||
@@ -301,7 +297,6 @@ class Track(Audio, Playable):
|
||||
Attributes:
|
||||
TAG (str): 'Directory'
|
||||
TYPE (str): 'track'
|
||||
art (str): Track artwork (/library/metadata/<ratingkey>/art/<artid>)
|
||||
chapterSource (TYPE): Unknown
|
||||
duration (int): Length of this album in seconds.
|
||||
grandparentArt (str): Album artist artwork.
|
||||
@@ -332,17 +327,10 @@ class Track(Audio, Playable):
|
||||
TAG = 'Track'
|
||||
TYPE = 'track'
|
||||
|
||||
_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):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
Audio._loadData(self, data)
|
||||
Playable._loadData(self, data)
|
||||
self._details_key = self.key + self._include
|
||||
self.art = data.attrib.get('art')
|
||||
self.chapterSource = data.attrib.get('chapterSource')
|
||||
self.duration = utils.cast(int, data.attrib.get('duration'))
|
||||
self.grandparentArt = data.attrib.get('grandparentArt')
|
||||
|
@@ -44,9 +44,9 @@ class PlexObject(object):
|
||||
self._server = server
|
||||
self._data = data
|
||||
self._initpath = initpath or self.key
|
||||
self._details_key = ''
|
||||
if data is not None:
|
||||
self._loadData(data)
|
||||
self._details_key = self._buildDetailsKey()
|
||||
|
||||
def __repr__(self):
|
||||
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))
|
||||
|
||||
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.
|
||||
"""
|
||||
try:
|
||||
@@ -89,6 +89,22 @@ class PlexObject(object):
|
||||
except UnknownType:
|
||||
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):
|
||||
""" 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
|
||||
@@ -203,9 +219,39 @@ class PlexObject(object):
|
||||
results.append(elem.attrib.get(attr))
|
||||
return results
|
||||
|
||||
def reload(self, key=None):
|
||||
""" Reload the data for this object from self.key. """
|
||||
key = key or self._details_key or self.key
|
||||
def reload(self, key=None, **kwargs):
|
||||
""" Reload the data for this object from 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:
|
||||
raise Unsupported('Cannot reload an object not built from a URL.')
|
||||
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
|
||||
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):
|
||||
return other is not None and self.key == other.key
|
||||
@@ -332,7 +399,7 @@ class PlexPartialObject(PlexObject):
|
||||
""" 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.
|
||||
object (main url) for that movie would contain.
|
||||
"""
|
||||
return not self.key or (self._details_key or self.key) == self._initpath
|
||||
|
||||
@@ -608,14 +675,6 @@ class Playable(object):
|
||||
self.accountID = utils.cast(int, data.attrib.get('accountID')) # history
|
||||
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):
|
||||
""" 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.
|
||||
|
||||
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'):
|
||||
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
|
||||
the Plex server. False will create a new filename with the format
|
||||
"<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
|
||||
function. If kwargs is not specified, the media items will be downloaded
|
||||
and saved to disk.
|
||||
|
@@ -53,7 +53,7 @@ class PlexClient(PlexObject):
|
||||
_token (str): Token used to access this client.
|
||||
_session (obj): Requests session object used to access this client.
|
||||
_proxyThroughServer (bool): Set to True after calling
|
||||
:func:`~plexapi.client.PlexClient.proxyThroughServer()` (default False).
|
||||
:func:`~plexapi.client.PlexClient.proxyThroughServer` (default False).
|
||||
"""
|
||||
TAG = 'Player'
|
||||
key = '/resources'
|
||||
@@ -138,7 +138,7 @@ class PlexClient(PlexObject):
|
||||
value (bool): Enable or disable proxying (optional, default True).
|
||||
|
||||
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:
|
||||
self._server = server
|
||||
@@ -171,7 +171,7 @@ class PlexClient(PlexObject):
|
||||
return ElementTree.fromstring(data) if data.strip() else None
|
||||
|
||||
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
|
||||
the response.
|
||||
|
||||
@@ -181,7 +181,7 @@ class PlexClient(PlexObject):
|
||||
**params (dict): Additional GET parameters to include with the command.
|
||||
|
||||
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('/')
|
||||
controller = command.split('/')[0]
|
||||
@@ -296,7 +296,7 @@ class PlexClient(PlexObject):
|
||||
**params (dict): Additional GET parameters to include with the command.
|
||||
|
||||
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:
|
||||
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
|
||||
|
||||
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:
|
||||
raise Unsupported('A server must be specified before using this command.')
|
||||
|
@@ -455,7 +455,7 @@ class LibrarySection(PlexObject):
|
||||
return self.fetchItems(key, **kwargs)
|
||||
|
||||
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))
|
||||
|
||||
@@ -517,7 +517,7 @@ class LibrarySection(PlexObject):
|
||||
def listChoices(self, category, libtype=None, **kwargs):
|
||||
""" Returns a list of :class:`~plexapi.library.FilterChoice` objects for the
|
||||
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.
|
||||
|
||||
Parameters:
|
||||
@@ -526,7 +526,7 @@ class LibrarySection(PlexObject):
|
||||
**kwargs (dict): Additional kwargs to narrow down the choices.
|
||||
|
||||
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?
|
||||
if category in kwargs:
|
||||
@@ -573,7 +573,7 @@ class LibrarySection(PlexObject):
|
||||
* year: List of years to search within ([yyyy, ...]). [all]
|
||||
|
||||
Raises:
|
||||
:class:`plexapi.exceptions.BadRequest`: when applying unknown filter
|
||||
:exc:`plexapi.exceptions.BadRequest`: when applying unknown filter
|
||||
"""
|
||||
# cleanup the core arguments
|
||||
args = {}
|
||||
@@ -659,20 +659,20 @@ class LibrarySection(PlexObject):
|
||||
def sync(self, policy, mediaSettings, client=None, clientId=None, title=None, sort=None, libtype=None,
|
||||
**kwargs):
|
||||
""" Add current library section as sync item for specified device.
|
||||
See description of :func:`~plexapi.library.LibrarySection.search()` for details about filtering / sorting
|
||||
and :func:`plexapi.myplex.MyPlexAccount.sync()` for possible exceptions.
|
||||
See description of :func:`~plexapi.library.LibrarySection.search` for details about filtering / sorting
|
||||
and :func:`~plexapi.myplex.MyPlexAccount.sync` for possible exceptions.
|
||||
|
||||
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
|
||||
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
|
||||
LibrarySection object.
|
||||
client (:class:`plexapi.myplex.MyPlexDevice`): 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
|
||||
client (:class:`~plexapi.myplex.MyPlexDevice`): 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
|
||||
generated from metadata of current media.
|
||||
sort (str): formatted as `column:dir`; column can be any of {`addedAt`, `originallyAvailableAt`,
|
||||
`lastViewedAt`, `titleSort`, `rating`, `mediaHeight`, `duration`}. dir can be `asc` or
|
||||
@@ -681,10 +681,10 @@ class LibrarySection(PlexObject):
|
||||
`track`).
|
||||
|
||||
Returns:
|
||||
:class:`plexapi.sync.SyncItem`: an instance of created syncItem.
|
||||
:class:`~plexapi.sync.SyncItem`: an instance of created syncItem.
|
||||
|
||||
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:
|
||||
|
||||
@@ -784,17 +784,17 @@ class MovieSection(LibrarySection):
|
||||
|
||||
def sync(self, videoQuality, limit=None, unwatched=False, **kwargs):
|
||||
""" 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
|
||||
:func:`plexapi.library.LibrarySection.sync()` for details on syncing libraries and possible exceptions.
|
||||
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.
|
||||
|
||||
Parameters:
|
||||
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`.
|
||||
unwatched (bool): if `True` watched videos wouldn't be synced.
|
||||
|
||||
Returns:
|
||||
:class:`plexapi.sync.SyncItem`: an instance of created syncItem.
|
||||
:class:`~plexapi.sync.SyncItem`: an instance of created syncItem.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -843,11 +843,11 @@ class ShowSection(LibrarySection):
|
||||
CONTENT_TYPE = 'video'
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
def recentlyAdded(self, libtype='episode', maxresults=50):
|
||||
@@ -877,17 +877,17 @@ class ShowSection(LibrarySection):
|
||||
|
||||
def sync(self, videoQuality, limit=None, unwatched=False, **kwargs):
|
||||
""" 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
|
||||
:func:`plexapi.library.LibrarySection.sync()` for details on syncing libraries and possible exceptions.
|
||||
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.
|
||||
|
||||
Parameters:
|
||||
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`.
|
||||
unwatched (bool): if `True` watched videos wouldn't be synced.
|
||||
|
||||
Returns:
|
||||
:class:`plexapi.sync.SyncItem`: an instance of created syncItem.
|
||||
:class:`~plexapi.sync.SyncItem`: an instance of created syncItem.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -941,15 +941,15 @@ class MusicSection(LibrarySection):
|
||||
return self.fetchItems(key)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
def all(self, libtype='artist', **kwargs):
|
||||
@@ -971,16 +971,16 @@ class MusicSection(LibrarySection):
|
||||
|
||||
def sync(self, bitrate, limit=None, **kwargs):
|
||||
""" 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
|
||||
:func:`plexapi.library.LibrarySection.sync()` for details on syncing libraries and possible exceptions.
|
||||
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.
|
||||
|
||||
Parameters:
|
||||
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`.
|
||||
|
||||
Returns:
|
||||
:class:`plexapi.sync.SyncItem`: an instance of created syncItem.
|
||||
:class:`~plexapi.sync.SyncItem`: an instance of created syncItem.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -1023,11 +1023,11 @@ class PhotoSection(LibrarySection):
|
||||
METADATA_TYPE = 'photo'
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
def all(self, libtype='photoalbum', **kwargs):
|
||||
@@ -1043,16 +1043,16 @@ class PhotoSection(LibrarySection):
|
||||
|
||||
def sync(self, resolution, limit=None, **kwargs):
|
||||
""" 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
|
||||
:func:`plexapi.library.LibrarySection.sync()` for details on syncing libraries and possible exceptions.
|
||||
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.
|
||||
|
||||
Parameters:
|
||||
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`.
|
||||
|
||||
Returns:
|
||||
:class:`plexapi.sync.SyncItem`: an instance of created syncItem.
|
||||
:class:`~plexapi.sync.SyncItem`: an instance of created syncItem.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -1079,7 +1079,7 @@ class PhotoSection(LibrarySection):
|
||||
class FilterChoice(PlexObject):
|
||||
""" 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
|
||||
:func:`~plexapi.library.LibrarySection.listChoices()`.
|
||||
:func:`~plexapi.library.LibrarySection.listChoices`.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Directory'
|
||||
@@ -1140,6 +1140,8 @@ class Collections(PlexPartialObject):
|
||||
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.
|
||||
@@ -1157,6 +1159,7 @@ class Collections(PlexPartialObject):
|
||||
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'
|
||||
@@ -1165,14 +1168,13 @@ class Collections(PlexPartialObject):
|
||||
|
||||
TAG = 'Directory'
|
||||
TYPE = 'collection'
|
||||
_include = "?includeExternalMedia=1&includePreferences=1"
|
||||
|
||||
def _loadData(self, data):
|
||||
self.ratingKey = utils.cast(int, data.attrib.get('ratingKey'))
|
||||
self.key = data.attrib.get('key').replace('/children', '') # FIX_BUG_50
|
||||
self._details_key = self.key + self._include
|
||||
self.addedAt = utils.toDatetime(data.attrib.get('addedAt'))
|
||||
self.art = data.attrib.get('art')
|
||||
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'))
|
||||
@@ -1189,14 +1191,16 @@ class Collections(PlexPartialObject):
|
||||
self.subtype = data.attrib.get('subtype')
|
||||
self.summary = data.attrib.get('summary')
|
||||
self.thumb = data.attrib.get('thumb')
|
||||
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'))
|
||||
|
||||
@property
|
||||
def children(self):
|
||||
return self.fetchItems(self.key)
|
||||
""" Returns a list of all items in the collection. """
|
||||
key = '/library/metadata/%s/children' % self.ratingKey
|
||||
return self.fetchItems(key)
|
||||
|
||||
@property
|
||||
def thumbUrl(self):
|
||||
@@ -1226,7 +1230,7 @@ class Collections(PlexPartialObject):
|
||||
collection = 'plexapi.library.Collections'
|
||||
collection.updateMode(mode="hide")
|
||||
"""
|
||||
mode_dict = {'default': '-2',
|
||||
mode_dict = {'default': '-1',
|
||||
'hide': '0',
|
||||
'hideItems': '1',
|
||||
'showItems': '2'}
|
||||
@@ -1293,3 +1297,54 @@ class Collections(PlexPartialObject):
|
||||
|
||||
# def edit(self, **kwargs):
|
||||
# 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')
|
||||
|
@@ -210,7 +210,6 @@ class MediaPartStream(PlexObject):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
self._data = data
|
||||
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')
|
||||
@@ -268,6 +267,7 @@ class VideoStream(MediaPartStream):
|
||||
self.cabac = cast(int, data.attrib.get('cabac'))
|
||||
self.chromaLocation = data.attrib.get('chromaLocation')
|
||||
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')
|
||||
@@ -326,11 +326,11 @@ class AudioStream(MediaPartStream):
|
||||
self.bitrate = cast(int, data.attrib.get('bitrate'))
|
||||
self.bitrateMode = data.attrib.get('bitrateMode')
|
||||
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.profile = data.attrib.get('profile')
|
||||
self.requiredBandwidths = data.attrib.get('requiredBandwidths')
|
||||
self.samplingRate = cast(int, data.attrib.get('samplingRate'))
|
||||
self.streamIdentifier = cast(int, data.attrib.get('streamIdentifier'))
|
||||
|
||||
# For Track only
|
||||
self.albumGain = cast(float, data.attrib.get('albumGain'))
|
||||
@@ -368,6 +368,7 @@ class SubtitleStream(MediaPartStream):
|
||||
self.headerCompression = data.attrib.get('headerCompression')
|
||||
self.key = data.attrib.get('key')
|
||||
self.requiredBandwidths = data.attrib.get('requiredBandwidths')
|
||||
self.transient = data.attrib.get('transient')
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
@@ -561,7 +562,7 @@ class MediaTag(PlexObject):
|
||||
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).
|
||||
<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.
|
||||
|
||||
* key (str): API URL to dig deeper into this tag (ex: /library/sections/1/all?actor=9081).
|
||||
@@ -588,7 +589,7 @@ class MediaTag(PlexObject):
|
||||
|
||||
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()`.
|
||||
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)
|
||||
|
@@ -544,7 +544,7 @@ class MyPlexAccount(PlexObject):
|
||||
return self.query(url, method=self._session.put, data=params)
|
||||
|
||||
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:
|
||||
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):
|
||||
""" 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:
|
||||
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.
|
||||
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 neither `client` nor `clientId` provided the clientId would be set to current clients`s identifier.
|
||||
|
||||
Returns:
|
||||
:class:`plexapi.sync.SyncItem`: an instance of created syncItem.
|
||||
:class:`~plexapi.sync.SyncItem`: an instance of created syncItem.
|
||||
|
||||
Raises:
|
||||
:class:`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`: when client with provided clientId wasn`t found.
|
||||
:exc:`plexapi.exceptions.BadRequest`: provided client doesn`t provides `sync-target`.
|
||||
"""
|
||||
if not client and not clientId:
|
||||
clientId = X_PLEX_IDENTIFIER
|
||||
@@ -686,7 +686,7 @@ class MyPlexAccount(PlexObject):
|
||||
|
||||
class MyPlexUser(PlexObject):
|
||||
""" 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
|
||||
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'
|
||||
accessToken (str): This resources accesstoken.
|
||||
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.
|
||||
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).
|
||||
@@ -930,7 +930,7 @@ class MyPlexResource(PlexObject):
|
||||
self.sourceTitle = data.attrib.get('sourceTitle') # owners plex username.
|
||||
|
||||
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.
|
||||
This function will prioritize local connections before remote and HTTPS before HTTP.
|
||||
After trying to connect to all available addresses for this resource and
|
||||
@@ -942,7 +942,7 @@ class MyPlexResource(PlexObject):
|
||||
HTTP or HTTPS connection.
|
||||
|
||||
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)
|
||||
# Only check non-local connections unless we own the resource
|
||||
@@ -965,7 +965,7 @@ class MyPlexResource(PlexObject):
|
||||
|
||||
class ResourceConnection(PlexObject):
|
||||
""" Represents a Resource Connection object found within the
|
||||
:class:`~myplex.MyPlexResource` objects.
|
||||
:class:`~plexapi.myplex.MyPlexResource` objects.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Connection'
|
||||
@@ -1049,7 +1049,7 @@ class MyPlexDevice(PlexObject):
|
||||
at least one connection was successful, the PlexClient object is built and returned.
|
||||
|
||||
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
|
||||
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)
|
||||
|
||||
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:
|
||||
: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:
|
||||
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.
|
||||
|
||||
- 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
|
||||
: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.run` and then at a later time call
|
||||
: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
|
||||
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
|
||||
finishes or expires. The parameter passed to the callback is the received authentication
|
||||
token or `None` if the login expired.
|
||||
|
@@ -168,20 +168,20 @@ class Photo(PlexPartialObject):
|
||||
|
||||
def sync(self, resolution, client=None, clientId=None, limit=None, title=None):
|
||||
""" 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:
|
||||
resolution (str): maximum allowed resolution for synchronized photos, see PHOTO_QUALITY_* values in the
|
||||
module :mod:`plexapi.sync`.
|
||||
client (:class:`plexapi.myplex.MyPlexDevice`): sync destination, see
|
||||
:func:`plexapi.myplex.MyPlexAccount.sync`.
|
||||
clientId (str): sync destination, see :func:`plexapi.myplex.MyPlexAccount.sync`.
|
||||
module :mod:`~plexapi.sync`.
|
||||
client (:class:`~plexapi.myplex.MyPlexDevice`): 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`.
|
||||
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.
|
||||
|
||||
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
|
||||
|
@@ -163,7 +163,7 @@ class Playlist(PlexPartialObject, Playable):
|
||||
**kwargs (dict): is passed to the filters. For a example see the search method.
|
||||
|
||||
Returns:
|
||||
:class:`plexapi.playlist.Playlist`: an instance of created Playlist.
|
||||
:class:`~plexapi.playlist.Playlist`: an instance of created Playlist.
|
||||
"""
|
||||
if smart:
|
||||
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,
|
||||
unwatched=False, title=None):
|
||||
""" 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:
|
||||
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
|
||||
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
|
||||
from the module :mod:`plexapi.sync`. Used only when playlist contains audio.
|
||||
client (:class:`plexapi.myplex.MyPlexDevice`): sync destination, see
|
||||
:func:`plexapi.myplex.MyPlexAccount.sync`.
|
||||
clientId (str): sync destination, see :func:`plexapi.myplex.MyPlexAccount.sync`.
|
||||
from the module :mod:`~plexapi.sync`. Used only when playlist contains audio.
|
||||
client (:class:`~plexapi.myplex.MyPlexDevice`): 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`.
|
||||
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.
|
||||
|
||||
Raises:
|
||||
:class:`plexapi.exceptions.BadRequest`: when playlist is not allowed to sync.
|
||||
:class:`plexapi.exceptions.Unsupported`: when playlist content is unsupported.
|
||||
:exc:`plexapi.exceptions.BadRequest`: when playlist is not allowed to sync.
|
||||
:exc:`plexapi.exceptions.Unsupported`: when playlist content is unsupported.
|
||||
|
||||
Returns:
|
||||
:class:`plexapi.sync.SyncItem`: an instance of created syncItem.
|
||||
:class:`~plexapi.sync.SyncItem`: an instance of created syncItem.
|
||||
"""
|
||||
|
||||
if not self.allowSync:
|
||||
|
@@ -8,7 +8,7 @@ from plexapi.base import PlexObject
|
||||
from plexapi.client import PlexClient
|
||||
from plexapi.compat import ElementTree, urlencode
|
||||
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.playlist import Playlist
|
||||
from plexapi.playqueue import PlayQueue
|
||||
@@ -185,7 +185,7 @@ class PlexServer(PlexObject):
|
||||
return Account(self, data)
|
||||
|
||||
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'
|
||||
if mediaType:
|
||||
key += '?mediaType=%s' % mediaType
|
||||
@@ -233,6 +233,53 @@ class PlexServer(PlexObject):
|
||||
log.warning('Unable to fetch client ports from myPlex: %s', err)
|
||||
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):
|
||||
""" Returns list of all :class:`~plexapi.client.PlexClient` objects connected to server. """
|
||||
items = []
|
||||
@@ -256,7 +303,7 @@ class PlexServer(PlexObject):
|
||||
name (str): Name of the client to return.
|
||||
|
||||
Raises:
|
||||
:class:`plexapi.exceptions.NotFound`: Unknown client name
|
||||
:exc:`plexapi.exceptions.NotFound`: Unknown client name
|
||||
"""
|
||||
for client in self.clients():
|
||||
if client and client.title == name:
|
||||
@@ -379,7 +426,7 @@ class PlexServer(PlexObject):
|
||||
title (str): Title of the playlist to return.
|
||||
|
||||
Raises:
|
||||
:class:`plexapi.exceptions.NotFound`: Invalid playlist title
|
||||
:exc:`plexapi.exceptions.NotFound`: Invalid playlist title
|
||||
"""
|
||||
return self.fetchItem('/playlists', title=title)
|
||||
|
||||
@@ -480,8 +527,8 @@ class PlexServer(PlexObject):
|
||||
Parameters:
|
||||
callback (func): Callback function to call on recieved messages.
|
||||
|
||||
raises:
|
||||
:class:`plexapi.exception.Unsupported`: Websocket-client not installed.
|
||||
Raises:
|
||||
:exc:`plexapi.exception.Unsupported`: Websocket-client not installed.
|
||||
"""
|
||||
notifier = AlertListener(self, callback)
|
||||
notifier.start()
|
||||
|
@@ -21,7 +21,10 @@ class Settings(PlexObject):
|
||||
|
||||
def __getattr__(self, attr):
|
||||
if attr.startswith('_'):
|
||||
return self.__dict__[attr]
|
||||
try:
|
||||
return self.__dict__[attr]
|
||||
except KeyError:
|
||||
raise AttributeError
|
||||
return self.get(attr).value
|
||||
|
||||
def __setattr__(self, attr, value):
|
||||
|
@@ -78,7 +78,7 @@ class SyncItem(PlexObject):
|
||||
self.location = data.find('Location').attrib.get('uri', '')
|
||||
|
||||
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]
|
||||
if len(server) == 0:
|
||||
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.
|
||||
|
||||
Raises:
|
||||
:class:`plexapi.exceptions.BadRequest`: when provided unknown video quality.
|
||||
:exc:`plexapi.exceptions.BadRequest`: when provided unknown video quality.
|
||||
"""
|
||||
if videoQuality == VIDEO_QUALITY_ORIGINAL:
|
||||
return MediaSettings('', '', '')
|
||||
@@ -231,7 +231,7 @@ class MediaSettings(object):
|
||||
module.
|
||||
|
||||
Raises:
|
||||
:class:`plexapi.exceptions.BadRequest` when provided unknown video quality.
|
||||
:exc:`plexapi.exceptions.BadRequest` when provided unknown video quality.
|
||||
"""
|
||||
if resolution in PHOTO_QUALITIES:
|
||||
return MediaSettings(photoQuality=PHOTO_QUALITIES[resolution], photoResolution=resolution)
|
||||
|
@@ -1,4 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import base64
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
@@ -147,7 +148,7 @@ def searchType(libtype):
|
||||
libtype (str): LibType to lookup (movie, show, season, episode, artist, album, track,
|
||||
collection)
|
||||
Raises:
|
||||
:class:`plexapi.exceptions.NotFound`: Unknown libtype
|
||||
:exc:`plexapi.exceptions.NotFound`: Unknown libtype
|
||||
"""
|
||||
libtype = compat.ustr(libtype)
|
||||
if libtype in [compat.ustr(v) for v in SEARCHTYPES.values()]:
|
||||
@@ -399,3 +400,7 @@ def getAgentIdentifier(section, agent):
|
||||
agents += identifiers
|
||||
raise NotFound('Couldnt find "%s" in agents list (%s)' %
|
||||
(agent, ', '.join(agents)))
|
||||
|
||||
|
||||
def base64str(text):
|
||||
return base64.b64encode(text.encode('utf-8')).decode('utf-8')
|
||||
|
@@ -13,6 +13,8 @@ class Video(PlexPartialObject):
|
||||
|
||||
Attributes:
|
||||
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>).
|
||||
lastViewedAt (datetime): Datetime item was last accessed.
|
||||
librarySectionID (int): :class:`~plexapi.library.LibrarySection` ID.
|
||||
@@ -20,6 +22,7 @@ class Video(PlexPartialObject):
|
||||
ratingKey (int): Unique key identifying this item.
|
||||
summary (str): Summary of the artist, track, or album.
|
||||
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.)
|
||||
titleSort (str): Title to use when sorting (defaults to title).
|
||||
type (str): 'artist', 'album', or 'track'.
|
||||
@@ -32,6 +35,8 @@ class Video(PlexPartialObject):
|
||||
self._data = data
|
||||
self.listType = 'video'
|
||||
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.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt'))
|
||||
self.librarySectionID = data.attrib.get('librarySectionID')
|
||||
@@ -40,6 +45,7 @@ class Video(PlexPartialObject):
|
||||
self.ratingKey = utils.cast(int, data.attrib.get('ratingKey'))
|
||||
self.summary = data.attrib.get('summary')
|
||||
self.thumb = data.attrib.get('thumb')
|
||||
self.thumbBlurHash = data.attrib.get('thumbBlurHash')
|
||||
self.title = data.attrib.get('title')
|
||||
self.titleSort = data.attrib.get('titleSort', self.title)
|
||||
self.type = data.attrib.get('type')
|
||||
@@ -201,21 +207,21 @@ class Video(PlexPartialObject):
|
||||
|
||||
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.
|
||||
See :func:`plexapi.myplex.MyPlexAccount.sync()` for possible exceptions.
|
||||
See :func:`~plexapi.myplex.MyPlexAccount.sync` for possible exceptions.
|
||||
|
||||
Parameters:
|
||||
videoQuality (int): idx of quality of the video, one of VIDEO_QUALITY_* values defined in
|
||||
:mod:`plexapi.sync` module.
|
||||
client (:class:`plexapi.myplex.MyPlexDevice`): sync destination, see
|
||||
:func:`plexapi.myplex.MyPlexAccount.sync`.
|
||||
clientId (str): sync destination, see :func:`plexapi.myplex.MyPlexAccount.sync`.
|
||||
:mod:`~plexapi.sync` module.
|
||||
client (:class:`~plexapi.myplex.MyPlexDevice`): 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`.
|
||||
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.
|
||||
|
||||
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
|
||||
@@ -277,17 +283,12 @@ class Movie(Playable, Video):
|
||||
TAG = 'Video'
|
||||
TYPE = 'movie'
|
||||
METADATA_TYPE = 'movie'
|
||||
_include = ('?checkFiles=1&includeExtras=1&includeRelated=1'
|
||||
'&includeOnDeck=1&includeChapters=1&includePopularLeaves=1'
|
||||
'&includeConcerts=1&includePreferences=1'
|
||||
'&includeBandwidths=1')
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
Video._loadData(self, data)
|
||||
Playable._loadData(self, data)
|
||||
|
||||
self._details_key = self.key + self._include
|
||||
self.art = data.attrib.get('art')
|
||||
self.audienceRating = utils.cast(float, data.attrib.get('audienceRating'))
|
||||
self.audienceRatingImage = data.attrib.get('audienceRatingImage')
|
||||
@@ -343,7 +344,7 @@ class Movie(Playable, Video):
|
||||
savepath (str): Defaults to current working dir.
|
||||
keep_original_name (bool): True to keep the original file name otherwise
|
||||
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 = []
|
||||
locations = [i for i in self.iterParts() if i]
|
||||
@@ -460,14 +461,14 @@ class Show(Video):
|
||||
def episode(self, title=None, season=None, episode=None):
|
||||
""" Find a episode using a title or season and episode.
|
||||
|
||||
Parameters:
|
||||
Parameters:
|
||||
title (str): Title of the episode to return
|
||||
season (int): Season number (default:None; required if title not specified).
|
||||
episode (int): Episode number (default:None; required if title not specified).
|
||||
|
||||
Raises:
|
||||
:class:`plexapi.exceptions.BadRequest`: If season and episode is missing.
|
||||
:class:`plexapi.exceptions.NotFound`: If the episode is missing.
|
||||
Raises:
|
||||
:exc:`plexapi.exceptions.BadRequest`: If season and episode is missing.
|
||||
:exc:`plexapi.exceptions.NotFound`: If the episode is missing.
|
||||
"""
|
||||
if title:
|
||||
key = '/library/metadata/%s/allLeaves' % self.ratingKey
|
||||
@@ -488,7 +489,7 @@ class Show(Video):
|
||||
return self.episodes(viewCount=0)
|
||||
|
||||
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)
|
||||
|
||||
def download(self, savepath=None, keep_original_name=False, **kwargs):
|
||||
@@ -498,7 +499,7 @@ class Show(Video):
|
||||
savepath (str): Defaults to current working dir.
|
||||
keep_original_name (bool): True to keep the original file name otherwise
|
||||
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 = []
|
||||
for episode in self.episodes():
|
||||
@@ -585,7 +586,7 @@ class Season(Video):
|
||||
return self.fetchItem(key, parentIndex=self.index, index=episode)
|
||||
|
||||
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)
|
||||
|
||||
def show(self):
|
||||
@@ -607,7 +608,7 @@ class Season(Video):
|
||||
savepath (str): Defaults to current working dir.
|
||||
keep_original_name (bool): True to keep the original file name otherwise
|
||||
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 = []
|
||||
for episode in self.episodes():
|
||||
@@ -656,16 +657,10 @@ class Episode(Playable, Video):
|
||||
TYPE = 'episode'
|
||||
METADATA_TYPE = 'episode'
|
||||
|
||||
_include = ('?checkFiles=1&includeExtras=1&includeRelated=1'
|
||||
'&includeOnDeck=1&includeChapters=1&includePopularLeaves=1'
|
||||
'&includeMarkers=1&includeConcerts=1&includePreferences=1'
|
||||
'&includeBandwidths=1')
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
Video._loadData(self, data)
|
||||
Playable._loadData(self, data)
|
||||
self._details_key = self.key + self._include
|
||||
self._seasonNumber = None # cached season number
|
||||
art = data.attrib.get('art')
|
||||
self.art = art if art and str(self.ratingKey) in art else None
|
||||
|
@@ -49,7 +49,7 @@ app = BUNDLE(
|
||||
bundle_identifier='com.Tautulli.Tautulli',
|
||||
version=VERSION,
|
||||
info_plist={
|
||||
'LSBackgroundOnly': True,
|
||||
'LSUIElement': True
|
||||
'LSUIElement': True,
|
||||
'NSHighResolutionCapable': True
|
||||
}
|
||||
)
|
||||
|
@@ -1,4 +1,5 @@
|
||||
pyinstaller==3.6
|
||||
pyopenssl
|
||||
pycryptodomex
|
||||
pyobjc-framework-Cocoa
|
||||
pyopenssl==20.0.0
|
||||
pycryptodomex==3.9.9
|
||||
pyobjc-framework-Cocoa==6.2.2
|
||||
pyobjc-core==6.2.2
|
||||
|
@@ -1,4 +1,4 @@
|
||||
pyinstaller==3.6
|
||||
pyopenssl
|
||||
pycryptodomex
|
||||
pywin32
|
||||
pyopenssl==20.0.0
|
||||
pycryptodomex==3.9.9
|
||||
pywin32==300
|
||||
|
@@ -98,6 +98,7 @@ CREATEPID = False
|
||||
PIDFILE = None
|
||||
NOFORK = False
|
||||
DOCKER = False
|
||||
SNAP = False
|
||||
FROZEN = False
|
||||
|
||||
SCHED = None
|
||||
@@ -194,6 +195,8 @@ def initialize(config_file):
|
||||
|
||||
if DOCKER:
|
||||
build = '[Docker] '
|
||||
elif SNAP:
|
||||
build = '[Snap] '
|
||||
elif FROZEN:
|
||||
build = '[Bundle] '
|
||||
else:
|
||||
|
@@ -134,6 +134,9 @@ _CONFIG_DEFINITIONS = {
|
||||
'HTTP_USERNAME': (str, 'General', ''),
|
||||
'HTTP_PLEX_ADMIN': (int, 'General', 0),
|
||||
'HTTP_BASE_URL': (str, 'General', ''),
|
||||
'HTTP_RATE_LIMIT_ATTEMPTS': (int, 'General', 10),
|
||||
'HTTP_RATE_LIMIT_ATTEMPTS_INTERVAL': (int, 'General', 300),
|
||||
'HTTP_RATE_LIMIT_LOCKOUT_TIME': (int, 'General', 300),
|
||||
'INTERFACE': (str, 'General', 'default'),
|
||||
'IMGUR_CLIENT_ID': (str, 'Monitoring', ''),
|
||||
'JOURNAL_MODE': (str, 'Advanced', 'WAL'),
|
||||
@@ -167,6 +170,7 @@ _CONFIG_DEFINITIONS = {
|
||||
'NOTIFY_REMOTE_ACCESS_THRESHOLD': (int, 'Monitoring', 60),
|
||||
'NOTIFY_CONCURRENT_BY_IP': (int, 'Monitoring', 0),
|
||||
'NOTIFY_CONCURRENT_THRESHOLD': (int, 'Monitoring', 2),
|
||||
'NOTIFY_NEW_DEVICE_INITIAL_ONLY': (int, 'Monitoring', 1),
|
||||
'PLEXPY_AUTO_UPDATE': (int, 'General', 0),
|
||||
'REFRESH_LIBRARIES_INTERVAL': (int, 'Monitoring', 12),
|
||||
'REFRESH_LIBRARIES_ON_STARTUP': (int, 'Monitoring', 1),
|
||||
@@ -253,7 +257,7 @@ def import_tautulli_config(config=None, backup=False):
|
||||
# Remove keys that should not be imported
|
||||
for key in _DO_NOT_IMPORT_KEYS:
|
||||
delattr(imported_config, key)
|
||||
if plexpy.DOCKER:
|
||||
if plexpy.DOCKER or plexpy.SNAP:
|
||||
for key in _DO_NOT_IMPORT_KEYS_DOCKER:
|
||||
delattr(imported_config, key)
|
||||
|
||||
@@ -536,3 +540,9 @@ class Config(object):
|
||||
self.JWT_UPDATE_SECRET = True
|
||||
|
||||
self.CONFIG_VERSION = 16
|
||||
|
||||
if self.CONFIG_VERSION == 16:
|
||||
if plexpy.SNAP:
|
||||
self.PLEXPY_AUTO_UPDATE = 0
|
||||
|
||||
self.CONFIG_VERSION = 17
|
||||
|
@@ -99,6 +99,7 @@ class DataFactory(object):
|
||||
'product',
|
||||
'player',
|
||||
'ip_address',
|
||||
'machine_id',
|
||||
'session_history.media_type',
|
||||
'session_history_metadata.rating_key',
|
||||
'session_history_metadata.parent_rating_key',
|
||||
@@ -151,6 +152,7 @@ class DataFactory(object):
|
||||
'product',
|
||||
'player',
|
||||
'ip_address',
|
||||
'machine_id',
|
||||
'media_type',
|
||||
'rating_key',
|
||||
'parent_rating_key',
|
||||
@@ -263,6 +265,7 @@ class DataFactory(object):
|
||||
'player': item['player'],
|
||||
'ip_address': item['ip_address'],
|
||||
'live': item['live'],
|
||||
'machine_id': item['machine_id'],
|
||||
'media_type': item['media_type'],
|
||||
'rating_key': item['rating_key'],
|
||||
'parent_rating_key': item['parent_rating_key'],
|
||||
@@ -1838,12 +1841,22 @@ class DataFactory(object):
|
||||
logger.warn("Tautulli DataFactory :: Unable to execute database query for delete_newsletter_log: %s." % e)
|
||||
return False
|
||||
|
||||
def get_user_devices(self, user_id=''):
|
||||
def get_user_devices(self, user_id='', history_only=True):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
if user_id:
|
||||
if history_only:
|
||||
query = 'SELECT machine_id FROM session_history ' \
|
||||
'WHERE user_id = ? ' \
|
||||
'GROUP BY machine_id'
|
||||
else:
|
||||
query = 'SELECT * FROM (' \
|
||||
'SELECT user_id, machine_id FROM session_history ' \
|
||||
'UNION SELECT user_id, machine_id from sessions_continued) ' \
|
||||
'WHERE user_id = ? ' \
|
||||
'GROUP BY machine_id'
|
||||
|
||||
try:
|
||||
query = 'SELECT machine_id FROM session_history WHERE user_id = ? GROUP BY machine_id'
|
||||
result = monitor_db.select(query=query, args=[user_id])
|
||||
except Exception as e:
|
||||
logger.warn("Tautulli DataFactory :: Unable to execute database query for get_user_devices: %s." % e)
|
||||
|
@@ -92,6 +92,14 @@ class Export(object):
|
||||
'collection': ['children'],
|
||||
'playlist': ['item']
|
||||
}
|
||||
TREE_MEDIA_TYPES = [
|
||||
('episode', 'season', 'show'),
|
||||
('track', 'album', 'artist'),
|
||||
('photo', 'photoalbum'),
|
||||
('clip', 'photoalbum'),
|
||||
('children', 'collection'),
|
||||
('item', 'playlist')
|
||||
]
|
||||
METADATA_LEVELS = (0, 1, 2, 3, 9)
|
||||
MEDIA_INFO_LEVELS = (0, 1, 2, 3, 9)
|
||||
IMAGE_LEVELS = (0, 1, 2, 9)
|
||||
@@ -128,6 +136,7 @@ class Export(object):
|
||||
self.file_size = 0
|
||||
self.exported_thumb = False
|
||||
self.exported_art = False
|
||||
self._reload_check_files = False
|
||||
|
||||
self.total_items = 0
|
||||
self.exported_items = 0
|
||||
@@ -288,7 +297,6 @@ class Export(object):
|
||||
},
|
||||
'audioStreams': {
|
||||
'codec': None,
|
||||
'codecID': None,
|
||||
'default': None,
|
||||
'displayTitle': None,
|
||||
'extendedDisplayTitle': None,
|
||||
@@ -305,15 +313,14 @@ class Export(object):
|
||||
'bitrate': None,
|
||||
'bitrateMode': None,
|
||||
'channels': None,
|
||||
'dialogNorm': None,
|
||||
'duration': None,
|
||||
'profile': None,
|
||||
'requiredBandwidths': None,
|
||||
'samplingRate': None
|
||||
'samplingRate': None,
|
||||
'streamIdentifier': None
|
||||
},
|
||||
'subtitleStreams': {
|
||||
'codec': None,
|
||||
'codecID': None,
|
||||
'default': None,
|
||||
'displayTitle': None,
|
||||
'extendedDisplayTitle': None,
|
||||
@@ -329,7 +336,8 @@ class Export(object):
|
||||
'forced': None,
|
||||
'format': None,
|
||||
'headerCompression': None,
|
||||
'key': None
|
||||
'key': None,
|
||||
'transient': None
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -595,7 +603,6 @@ class Export(object):
|
||||
},
|
||||
'audioStreams': {
|
||||
'codec': None,
|
||||
'codecID': None,
|
||||
'default': None,
|
||||
'displayTitle': None,
|
||||
'extendedDisplayTitle': None,
|
||||
@@ -612,15 +619,14 @@ class Export(object):
|
||||
'bitrate': None,
|
||||
'bitrateMode': None,
|
||||
'channels': None,
|
||||
'dialogNorm': None,
|
||||
'duration': None,
|
||||
'profile': None,
|
||||
'requiredBandwidths': None,
|
||||
'samplingRate': None
|
||||
'samplingRate': None,
|
||||
'streamIdentifier': None
|
||||
},
|
||||
'subtitleStreams': {
|
||||
'codec': None,
|
||||
'codecID': None,
|
||||
'default': None,
|
||||
'displayTitle': None,
|
||||
'extendedDisplayTitle': None,
|
||||
@@ -636,7 +642,8 @@ class Export(object):
|
||||
'forced': None,
|
||||
'format': None,
|
||||
'headerCompression': None,
|
||||
'key': None
|
||||
'key': None,
|
||||
'transient': None
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -829,7 +836,6 @@ class Export(object):
|
||||
'syncState': None,
|
||||
'audioStreams': {
|
||||
'codec': None,
|
||||
'codecID': None,
|
||||
'default': None,
|
||||
'displayTitle': None,
|
||||
'extendedDisplayTitle': None,
|
||||
@@ -857,7 +863,6 @@ class Export(object):
|
||||
},
|
||||
'lyricStreams': {
|
||||
'codec': None,
|
||||
'codecID': None,
|
||||
'default': None,
|
||||
'displayTitle': None,
|
||||
'extendedDisplayTitle': None,
|
||||
@@ -1097,7 +1102,7 @@ class Export(object):
|
||||
'media.optimizedVersion', 'media.hdr'
|
||||
],
|
||||
2: [
|
||||
'media.parts.accessible', 'media.parts.exists', 'media.parts.file', 'media.parts.duration',
|
||||
'media.parts.file', 'media.parts.duration',
|
||||
'media.parts.container', 'media.parts.indexes', 'media.parts.size', 'media.parts.sizeHuman',
|
||||
'media.parts.audioProfile', 'media.parts.videoProfile',
|
||||
'media.parts.optimizedForStreaming', 'media.parts.deepAnalysisVersion'
|
||||
@@ -1208,7 +1213,7 @@ class Export(object):
|
||||
'media.optimizedVersion', 'media.hdr'
|
||||
],
|
||||
2: [
|
||||
'media.parts.accessible', 'media.parts.exists', 'media.parts.file', 'media.parts.duration',
|
||||
'media.parts.file', 'media.parts.duration',
|
||||
'media.parts.container', 'media.parts.indexes', 'media.parts.size', 'media.parts.sizeHuman',
|
||||
'media.parts.audioProfile', 'media.parts.videoProfile',
|
||||
'media.parts.optimizedForStreaming', 'media.parts.deepAnalysisVersion'
|
||||
@@ -1317,7 +1322,7 @@ class Export(object):
|
||||
'media.bitrate', 'media.container', 'media.duration'
|
||||
],
|
||||
2: [
|
||||
'media.parts.accessible', 'media.parts.exists', 'media.parts.file', 'media.parts.duration',
|
||||
'media.parts.file', 'media.parts.duration',
|
||||
'media.parts.container', 'media.parts.size', 'media.parts.sizeHuman',
|
||||
'media.parts.audioProfile',
|
||||
'media.parts.deepAnalysisVersion', 'media.parts.hasThumbnail'
|
||||
@@ -1392,7 +1397,7 @@ class Export(object):
|
||||
'media.iso', 'media.lens', 'media.make', 'media.model'
|
||||
],
|
||||
2: [
|
||||
'media.parts.accessible', 'media.parts.exists', 'media.parts.file',
|
||||
'media.parts.file',
|
||||
'media.parts.container', 'media.parts.size', 'media.parts.sizeHuman'
|
||||
],
|
||||
3: [
|
||||
@@ -1822,6 +1827,15 @@ class Export(object):
|
||||
else:
|
||||
self._custom_fields[media_type] = {field}
|
||||
|
||||
for tree in self.TREE_MEDIA_TYPES:
|
||||
for child_media_type, parent_media_type in zip(tree[:-1], tree[1:]):
|
||||
if child_media_type in self._custom_fields:
|
||||
plural_child_media_type = self.PLURAL_MEDIA_TYPES[child_media_type]
|
||||
if parent_media_type in self._custom_fields:
|
||||
self._custom_fields[parent_media_type].add(plural_child_media_type)
|
||||
else:
|
||||
self._custom_fields[parent_media_type] = {plural_child_media_type}
|
||||
|
||||
def _parse_custom_field(self, media_type, field):
|
||||
for child_media_type in self.CHILD_MEDIA_TYPES.get(media_type, []):
|
||||
plural_key = self.PLURAL_MEDIA_TYPES[child_media_type]
|
||||
@@ -1871,6 +1885,10 @@ class Export(object):
|
||||
elif self.media_type == 'playlist' and 'item' in self._custom_fields:
|
||||
export_attrs_set.update(self._custom_fields['item'])
|
||||
|
||||
if 'media.parts.accessible' in export_attrs_set or 'media.parts.exists' in export_attrs_set or \
|
||||
self.media_info_level == 9:
|
||||
self._reload_check_files = True
|
||||
|
||||
for attr in export_attrs_set:
|
||||
try:
|
||||
value = helpers.get_dict_value_by_path(media_attrs, attr)
|
||||
@@ -1958,12 +1976,13 @@ class Export(object):
|
||||
pass
|
||||
|
||||
def export_obj(self, obj):
|
||||
# Reload ~plexapi.base.PlexPartialObject
|
||||
if hasattr(obj, 'isPartialObject') and obj.isPartialObject():
|
||||
obj = obj.reload()
|
||||
|
||||
media_type = self._media_type(obj)
|
||||
export_attrs = self._get_export_attrs(media_type)
|
||||
|
||||
# Reload ~plexapi.base.PlexPartialObject
|
||||
if hasattr(obj, 'isPartialObject') and obj.isPartialObject():
|
||||
obj = obj.reload(checkFiles=self._reload_check_files)
|
||||
|
||||
return helpers.get_attrs_to_dict(obj, attrs=export_attrs)
|
||||
|
||||
def get_any_hdr(self, item, media_type):
|
||||
|
@@ -19,11 +19,11 @@ from __future__ import unicode_literals
|
||||
from future.builtins import object
|
||||
from future.builtins import str
|
||||
|
||||
from functools import partial
|
||||
from multiprocessing.dummy import Pool as ThreadPool
|
||||
from future.moves.urllib.parse import urljoin
|
||||
|
||||
import certifi
|
||||
import requests
|
||||
import urllib3
|
||||
|
||||
import plexpy
|
||||
@@ -41,6 +41,7 @@ class HTTPHandler(object):
|
||||
"""
|
||||
|
||||
def __init__(self, urls, headers=None, token=None, timeout=10, ssl_verify=True, silent=False):
|
||||
self._valid_request_types = {'GET', 'POST', 'PUT', 'DELETE'}
|
||||
self._silent = silent
|
||||
|
||||
if isinstance(urls, str):
|
||||
@@ -51,24 +52,34 @@ class HTTPHandler(object):
|
||||
if headers:
|
||||
self.headers = headers
|
||||
else:
|
||||
self.headers = {'X-Plex-Product': plexpy.common.PRODUCT,
|
||||
'X-Plex-Version': plexpy.common.RELEASE,
|
||||
'X-Plex-Client-Identifier': plexpy.CONFIG.PMS_UUID,
|
||||
'X-Plex-Platform': plexpy.common.PLATFORM,
|
||||
'X-Plex-Platform-Version': plexpy.common.PLATFORM_RELEASE,
|
||||
'X-Plex-Device': '{} {}'.format(plexpy.common.PLATFORM,
|
||||
plexpy.common.PLATFORM_RELEASE),
|
||||
'X-Plex-Device-Name': plexpy.common.PLATFORM_DEVICE_NAME
|
||||
}
|
||||
self.headers = {
|
||||
'X-Plex-Product': plexpy.common.PRODUCT,
|
||||
'X-Plex-Version': plexpy.common.RELEASE,
|
||||
'X-Plex-Client-Identifier': plexpy.CONFIG.PMS_UUID,
|
||||
'X-Plex-Platform': plexpy.common.PLATFORM,
|
||||
'X-Plex-Platform-Version': plexpy.common.PLATFORM_RELEASE,
|
||||
'X-Plex-Device': '{} {}'.format(plexpy.common.PLATFORM,
|
||||
plexpy.common.PLATFORM_RELEASE),
|
||||
'X-Plex-Device-Name': plexpy.common.PLATFORM_DEVICE_NAME
|
||||
}
|
||||
|
||||
self.token = token
|
||||
if self.token:
|
||||
self.headers['X-Plex-Token'] = self.token
|
||||
|
||||
self._session = requests.Session()
|
||||
self.timeout = timeout
|
||||
self.ssl_verify = ssl_verify
|
||||
self.ssl_verify = certifi.where() if ssl_verify else False
|
||||
if not self.ssl_verify:
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
self.valid_request_types = ('GET', 'POST', 'PUT', 'DELETE')
|
||||
self.uri = None
|
||||
self.data = None
|
||||
self.request_type = 'GET'
|
||||
self.output_format = 'raw'
|
||||
self.return_type = False
|
||||
self.callback = None
|
||||
self.request_kwargs = {}
|
||||
|
||||
def make_request(self,
|
||||
uri=None,
|
||||
@@ -96,7 +107,7 @@ class HTTPHandler(object):
|
||||
self.timeout = timeout or self.timeout
|
||||
self.request_kwargs = request_kwargs
|
||||
|
||||
if self.request_type not in self.valid_request_types:
|
||||
if self.request_type not in self._valid_request_types:
|
||||
logger.debug("HTTP request made but unsupported request type given.")
|
||||
return None
|
||||
|
||||
@@ -115,7 +126,7 @@ class HTTPHandler(object):
|
||||
return responses[0]
|
||||
|
||||
else:
|
||||
logger.debug("HTTP request made but no enpoint given.")
|
||||
logger.debug("HTTP request made but no uri endpoint provided.")
|
||||
return None
|
||||
|
||||
def _http_requests_pool(self, urls, workers=10, chunk=None):
|
||||
@@ -128,20 +139,13 @@ class HTTPHandler(object):
|
||||
if len(urls) == 0:
|
||||
chunk = 0
|
||||
|
||||
if self.ssl_verify:
|
||||
session = urllib3.PoolManager(cert_reqs=2, ca_certs=certifi.where()) # ssl.CERT_REQUIRED = 2
|
||||
else:
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
session = urllib3.PoolManager()
|
||||
part = partial(self._http_requests_urllib3, session=session)
|
||||
|
||||
if len(urls) == 1:
|
||||
yield part(urls[0])
|
||||
yield self._http_requests_single(urls[0])
|
||||
else:
|
||||
pool = ThreadPool(workers)
|
||||
|
||||
try:
|
||||
for work in pool.imap_unordered(part, urls, chunk):
|
||||
for work in pool.imap_unordered(self._http_requests_single, urls, chunk):
|
||||
yield work
|
||||
except Exception as e:
|
||||
if not self._silent:
|
||||
@@ -150,34 +154,40 @@ class HTTPHandler(object):
|
||||
pool.close()
|
||||
pool.join()
|
||||
|
||||
def _http_requests_urllib3(self, url, session):
|
||||
def _http_requests_single(self, url):
|
||||
"""Request the data from the url"""
|
||||
error_msg = "Failed to access uri endpoint %s. " % self.uri
|
||||
try:
|
||||
r = session.request(self.request_type, url, headers=self.headers, fields=self.data,
|
||||
timeout=self.timeout, **self.request_kwargs)
|
||||
except IOError as e:
|
||||
r = self._session.request(self.request_type, url, headers=self.headers, data=self.data,
|
||||
timeout=self.timeout, verify=self.ssl_verify, **self.request_kwargs)
|
||||
r.raise_for_status()
|
||||
except requests.exceptions.Timeout as e:
|
||||
if not self._silent:
|
||||
logger.warn("Failed to access uri endpoint %s with error %s" % (self.uri, e))
|
||||
logger.error(error_msg + "Request timed out: %s", e)
|
||||
return None
|
||||
except Exception as e:
|
||||
except requests.exceptions.SSLError as e:
|
||||
if not self._silent:
|
||||
logger.warn("Failed to access uri endpoint %s. Is your server maybe accepting SSL connections only? %s" % (self.uri, e))
|
||||
logger.error(error_msg + "Is your server maybe accepting SSL connections only? %s", e)
|
||||
return None
|
||||
except:
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if not self._silent:
|
||||
logger.warn("Failed to access uri endpoint %s with Uncaught exception." % self.uri)
|
||||
logger.error(error_msg + "Status code %s", e)
|
||||
return None
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
if not self._silent:
|
||||
logger.error(error_msg + "Connection error: %s", e)
|
||||
return None
|
||||
except requests.exceptions.RequestException as e:
|
||||
if not self._silent:
|
||||
logger.error(error_msg + "Uncaught exception: %s", e)
|
||||
return None
|
||||
|
||||
response_status = r.status
|
||||
response_content = r.data
|
||||
response_status = r.status_code
|
||||
response_content = r.content
|
||||
response_headers = r.headers
|
||||
|
||||
if response_status in (200, 201):
|
||||
return self._http_format_output(response_content, response_headers)
|
||||
else:
|
||||
if not self._silent:
|
||||
logger.warn("Failed to access uri endpoint %s. Status code %r" % (self.uri, response_status))
|
||||
return None
|
||||
|
||||
def _http_format_output(self, response_content, response_headers):
|
||||
"""Formats the request response to the desired type"""
|
||||
|
@@ -44,11 +44,12 @@ else:
|
||||
class MacOSSystemTray(object):
|
||||
def __init__(self):
|
||||
self.image_dir = os.path.join(plexpy.PROG_DIR, 'data/interfaces/', plexpy.CONFIG.INTERFACE, 'images')
|
||||
self.icon = os.path.join(self.image_dir, 'logo-flat-white.ico')
|
||||
|
||||
if plexpy.UPDATE_AVAILABLE:
|
||||
self.icon = os.path.join(self.image_dir, 'logo-circle-update.ico')
|
||||
self.update_title = 'Check for Updates - Update Available!'
|
||||
else:
|
||||
self.icon = os.path.join(self.image_dir, 'logo-circle.ico')
|
||||
self.update_title = 'Check for Updates'
|
||||
|
||||
self.menu = [
|
||||
rumps.MenuItem('Open Tautulli', callback=self.tray_open),
|
||||
@@ -56,7 +57,7 @@ class MacOSSystemTray(object):
|
||||
rumps.MenuItem('Start Tautulli at Login', callback=self.tray_startup),
|
||||
rumps.MenuItem('Open Browser when Tautulli Starts', callback=self.tray_browser),
|
||||
None,
|
||||
rumps.MenuItem('Check for Updates', callback=self.tray_check_update),
|
||||
rumps.MenuItem(self.update_title, callback=self.tray_check_update),
|
||||
rumps.MenuItem('Restart', callback=self.tray_restart),
|
||||
rumps.MenuItem('Quit', callback=self.tray_quit)
|
||||
]
|
||||
@@ -65,7 +66,8 @@ class MacOSSystemTray(object):
|
||||
self.menu[2].state = plexpy.CONFIG.LAUNCH_STARTUP
|
||||
self.menu[3].state = plexpy.CONFIG.LAUNCH_BROWSER
|
||||
|
||||
self.tray_icon = rumps.App(common.PRODUCT, icon=self.icon, menu=self.menu, quit_button=None)
|
||||
self.tray_icon = rumps.App(common.PRODUCT, icon=self.icon, template=True,
|
||||
menu=self.menu, quit_button=None)
|
||||
|
||||
def start(self):
|
||||
logger.info("Launching MacOS menu bar icon.")
|
||||
@@ -98,6 +100,9 @@ class MacOSSystemTray(object):
|
||||
def tray_update(self, tray_icon):
|
||||
if plexpy.UPDATE_AVAILABLE:
|
||||
plexpy.SIGNAL = 'update'
|
||||
else:
|
||||
self.update_title = 'Check for Updates - No Update Available'
|
||||
self.menu[5].title = self.update_title
|
||||
|
||||
def tray_restart(self, tray_icon):
|
||||
plexpy.SIGNAL = 'restart'
|
||||
@@ -107,10 +112,10 @@ class MacOSSystemTray(object):
|
||||
|
||||
def change_tray_update_icon(self):
|
||||
if plexpy.UPDATE_AVAILABLE:
|
||||
self.icon = os.path.join(self.image_dir, 'logo-circle-update.ico')
|
||||
self.update_title = 'Check for Updates - Update Available!'
|
||||
else:
|
||||
self.icon = os.path.join(self.image_dir, 'logo-circle.ico')
|
||||
self.update(icon=self.icon)
|
||||
self.update_title = 'Check for Updates'
|
||||
self.menu[5].title = self.update_title
|
||||
|
||||
def change_tray_icons(self):
|
||||
self.tray_icon.menu['Start Tautulli at Login'].state = plexpy.CONFIG.LAUNCH_STARTUP
|
||||
|
@@ -193,7 +193,8 @@ def notify_conditions(notify_action=None, stream_data=None, timeline_data=None):
|
||||
|
||||
elif notify_action == 'on_newdevice':
|
||||
data_factory = datafactory.DataFactory()
|
||||
user_devices = data_factory.get_user_devices(user_id=stream_data['user_id'])
|
||||
user_devices = data_factory.get_user_devices(user_id=stream_data['user_id'],
|
||||
history_only=not plexpy.CONFIG.NOTIFY_NEW_DEVICE_INITIAL_ONLY)
|
||||
evaluated = stream_data['machine_id'] not in user_devices
|
||||
|
||||
elif stream_data['media_type'] in ('movie', 'episode', 'clip'):
|
||||
|
@@ -854,8 +854,8 @@ class Notifier(object):
|
||||
|
||||
else:
|
||||
verify_msg = ""
|
||||
if response is not None and response.status_code >= 400 and response.status_code < 500:
|
||||
verify_msg = " Verify you notification agent settings are correct."
|
||||
if response is not None and 400 <= response.status_code < 500:
|
||||
verify_msg = " Verify your notification agent settings are correct."
|
||||
|
||||
logger.error("Tautulli Notifiers :: {name} notification failed.{msg}".format(msg=verify_msg, name=self.NAME))
|
||||
|
||||
|
@@ -213,6 +213,7 @@ def mask_session_info(list_of_dicts, mask_metadata=True):
|
||||
|
||||
keys_to_mask = {'user_id': '',
|
||||
'user': 'Plex User',
|
||||
'username': 'Plex User',
|
||||
'friendly_name': 'Plex User',
|
||||
'user_thumb': common.DEFAULT_USER_THUMB,
|
||||
'ip_address': 'N/A',
|
||||
|
@@ -18,4 +18,4 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
PLEXPY_BRANCH = "master"
|
||||
PLEXPY_RELEASE_VERSION = "v2.6.1"
|
||||
PLEXPY_RELEASE_VERSION = "v2.6.4"
|
@@ -133,7 +133,13 @@ def get_version():
|
||||
return cur_commit_hash, remote_name, branch_name
|
||||
|
||||
else:
|
||||
plexpy.INSTALL_TYPE = 'docker' if plexpy.DOCKER else 'source'
|
||||
if plexpy.DOCKER:
|
||||
plexpy.INSTALL_TYPE = 'docker'
|
||||
elif plexpy.SNAP:
|
||||
plexpy.INSTALL_TYPE = 'snap'
|
||||
else:
|
||||
plexpy.INSTALL_TYPE = 'source'
|
||||
|
||||
current_version, current_branch = get_version_from_file()
|
||||
return current_version, 'origin', current_branch
|
||||
|
||||
@@ -162,10 +168,13 @@ def check_update(scheduler=False, notify=False, use_cache=False):
|
||||
|
||||
if not plexpy.CURRENT_VERSION:
|
||||
plexpy.UPDATE_AVAILABLE = None
|
||||
elif plexpy.COMMITS_BEHIND > 0 and (plexpy.common.BRANCH in ('master', 'beta') or plexpy.FROZEN) and \
|
||||
elif plexpy.COMMITS_BEHIND > 0 and \
|
||||
(plexpy.common.BRANCH in ('master', 'beta') or plexpy.SNAP or plexpy.FROZEN) and \
|
||||
plexpy.common.RELEASE != plexpy.LATEST_RELEASE:
|
||||
plexpy.UPDATE_AVAILABLE = 'release'
|
||||
elif plexpy.COMMITS_BEHIND > 0 and plexpy.CURRENT_VERSION != plexpy.LATEST_VERSION and not plexpy.FROZEN:
|
||||
elif plexpy.COMMITS_BEHIND > 0 and \
|
||||
not plexpy.SNAP and not plexpy.FROZEN and \
|
||||
plexpy.CURRENT_VERSION != plexpy.LATEST_VERSION:
|
||||
plexpy.UPDATE_AVAILABLE = 'commit'
|
||||
else:
|
||||
plexpy.UPDATE_AVAILABLE = False
|
||||
@@ -265,7 +274,11 @@ def check_github(scheduler=False, notify=False, use_cache=False):
|
||||
'plexpy_update_commit': plexpy.LATEST_VERSION,
|
||||
'plexpy_update_behind': plexpy.COMMITS_BEHIND})
|
||||
|
||||
if scheduler and plexpy.CONFIG.PLEXPY_AUTO_UPDATE and not plexpy.DOCKER and not plexpy.FROZEN:
|
||||
if plexpy.PYTHON2:
|
||||
logger.warn('Tautulli is running using Python 2. Unable to run automatic update.')
|
||||
|
||||
elif scheduler and plexpy.CONFIG.PLEXPY_AUTO_UPDATE and \
|
||||
not plexpy.DOCKER and not plexpy.SNAP and not plexpy.FROZEN:
|
||||
logger.info('Running automatic update.')
|
||||
plexpy.shutdown(restart=True, update=True)
|
||||
|
||||
@@ -276,10 +289,14 @@ def check_github(scheduler=False, notify=False, use_cache=False):
|
||||
|
||||
|
||||
def update():
|
||||
if plexpy.PYTHON2:
|
||||
logger.warn('Tautulli is running using Python 2. Unable to update.')
|
||||
return
|
||||
|
||||
if not plexpy.UPDATE_AVAILABLE:
|
||||
return
|
||||
|
||||
if plexpy.INSTALL_TYPE in ('docker', 'windows', 'macos'):
|
||||
if plexpy.INSTALL_TYPE in ('docker', 'snap', 'windows', 'macos'):
|
||||
return
|
||||
|
||||
elif plexpy.INSTALL_TYPE == 'git':
|
||||
|
@@ -16,7 +16,7 @@
|
||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
# http://tools.cherrypy.org/wiki/AuthenticationAndAccessRestrictions
|
||||
# https://github.com/cherrypy/tools/blob/master/AuthenticationAndAccessRestrictions
|
||||
# Form based authentication for CherryPy. Requires the
|
||||
# Session tool to be loaded.
|
||||
|
||||
@@ -33,11 +33,13 @@ import plexpy
|
||||
if plexpy.PYTHON2:
|
||||
import logger
|
||||
from database import MonitorDatabase
|
||||
from helpers import timestamp
|
||||
from users import Users, refresh_users
|
||||
from plextv import PlexTV
|
||||
else:
|
||||
from plexpy import logger
|
||||
from plexpy.database import MonitorDatabase
|
||||
from plexpy.helpers import timestamp
|
||||
from plexpy.users import Users, refresh_users
|
||||
from plexpy.plextv import PlexTV
|
||||
|
||||
@@ -246,6 +248,33 @@ def all_of(*conditions):
|
||||
return check
|
||||
|
||||
|
||||
def check_rate_limit(ip_address):
|
||||
monitor_db = MonitorDatabase()
|
||||
result = monitor_db.select('SELECT timestamp, success FROM user_login '
|
||||
'WHERE ip_address = ? '
|
||||
'AND timestamp >= ( '
|
||||
'SELECT CASE WHEN MAX(timestamp) IS NULL THEN 0 ELSE MAX(timestamp) END '
|
||||
'FROM user_login WHERE ip_address = ? AND success = 1) '
|
||||
'ORDER BY timestamp DESC',
|
||||
[ip_address, ip_address])
|
||||
|
||||
try:
|
||||
last_timestamp = result[0]['timestamp']
|
||||
except IndexError:
|
||||
last_timestamp = 0
|
||||
|
||||
try:
|
||||
last_success = max(login['timestamp'] for login in result if login['success'])
|
||||
except ValueError:
|
||||
last_success = 0
|
||||
|
||||
max_timestamp = max(last_success, last_timestamp - plexpy.CONFIG.HTTP_RATE_LIMIT_ATTEMPTS_INTERVAL)
|
||||
attempts = [login for login in result if login['timestamp'] >= max_timestamp and not login['success']]
|
||||
|
||||
if len(attempts) >= plexpy.CONFIG.HTTP_RATE_LIMIT_ATTEMPTS:
|
||||
return max(last_timestamp - (timestamp() - plexpy.CONFIG.HTTP_RATE_LIMIT_LOCKOUT_TIME), 0)
|
||||
|
||||
|
||||
# Controller to provide login and logout actions
|
||||
|
||||
class AuthController(object):
|
||||
@@ -325,6 +354,16 @@ class AuthController(object):
|
||||
cherrypy.response.status = 405
|
||||
return {'status': 'error', 'message': 'Sign in using POST.'}
|
||||
|
||||
ip_address = cherrypy.request.remote.ip
|
||||
rate_limit = check_rate_limit(ip_address)
|
||||
|
||||
if rate_limit:
|
||||
logger.debug("Tautulli WebAuth :: Too many incorrect login attempts from '%s'." % ip_address)
|
||||
error_message = {'status': 'error', 'message': 'Too many login attempts.'}
|
||||
cherrypy.response.status = 429
|
||||
cherrypy.response.headers['Retry-After'] = rate_limit
|
||||
return error_message
|
||||
|
||||
error_message = {'status': 'error', 'message': 'Invalid credentials.'}
|
||||
|
||||
valid_login, user_details, user_group = check_credentials(username=username,
|
||||
|
@@ -1871,6 +1871,7 @@ class WebInterface(object):
|
||||
"guid": "com.plexapp.agents.thetvdb://121361/6/1?lang=en",
|
||||
"ip_address": "xxx.xxx.xxx.xxx",
|
||||
"live": 0,
|
||||
"machine_id": "lmd93nkn12k29j2lnm",
|
||||
"media_index": 17,
|
||||
"media_type": "episode",
|
||||
"originally_available_at": "2016-04-24",
|
||||
@@ -3135,6 +3136,7 @@ class WebInterface(object):
|
||||
"notify_concurrent_by_ip": checked(plexpy.CONFIG.NOTIFY_CONCURRENT_BY_IP),
|
||||
"notify_concurrent_threshold": plexpy.CONFIG.NOTIFY_CONCURRENT_THRESHOLD,
|
||||
"notify_continued_session_threshold": plexpy.CONFIG.NOTIFY_CONTINUED_SESSION_THRESHOLD,
|
||||
"notify_new_device_initial_only": checked(plexpy.CONFIG.NOTIFY_NEW_DEVICE_INITIAL_ONLY),
|
||||
"home_sections": json.dumps(plexpy.CONFIG.HOME_SECTIONS),
|
||||
"home_stats_cards": json.dumps(plexpy.CONFIG.HOME_STATS_CARDS),
|
||||
"home_library_cards": json.dumps(plexpy.CONFIG.HOME_LIBRARY_CARDS),
|
||||
@@ -3198,6 +3200,7 @@ class WebInterface(object):
|
||||
"refresh_libraries_on_startup", "refresh_users_on_startup",
|
||||
"notify_consecutive", "notify_recently_added_upgrade",
|
||||
"notify_group_recently_added_grandparent", "notify_group_recently_added_parent",
|
||||
"notify_new_device_initial_only",
|
||||
"monitor_pms_updates", "get_file_sizes", "log_blacklist", "http_hash_password",
|
||||
"allow_guest_access", "cache_images", "http_proxy", "http_basic_auth", "notify_concurrent_by_ip",
|
||||
"history_table_activity", "plexpy_auto_update",
|
||||
@@ -4306,7 +4309,7 @@ class WebInterface(object):
|
||||
plexpy.CONFIG.GIT_REPO,
|
||||
plexpy.CURRENT_VERSION,
|
||||
plexpy.LATEST_VERSION))
|
||||
}
|
||||
}
|
||||
|
||||
else:
|
||||
update = {'result': 'success',
|
||||
@@ -4314,7 +4317,7 @@ class WebInterface(object):
|
||||
'message': 'Tautulli is up to date.'
|
||||
}
|
||||
|
||||
if plexpy.DOCKER or plexpy.FROZEN:
|
||||
if plexpy.DOCKER or plexpy.SNAP or plexpy.FROZEN:
|
||||
update['install_type'] = plexpy.INSTALL_TYPE
|
||||
|
||||
return update
|
||||
@@ -4348,7 +4351,9 @@ class WebInterface(object):
|
||||
@cherrypy.expose
|
||||
@requireAuth(member_of("admin"))
|
||||
def update(self, **kwargs):
|
||||
if plexpy.DOCKER:
|
||||
if plexpy.PYTHON2:
|
||||
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT + "home?update=python2")
|
||||
if plexpy.DOCKER or plexpy.SNAP:
|
||||
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT + "home")
|
||||
|
||||
# Show changelog after updating
|
||||
|
@@ -43,13 +43,14 @@ else:
|
||||
class WindowsSystemTray(object):
|
||||
def __init__(self):
|
||||
self.image_dir = os.path.join(plexpy.PROG_DIR, 'data/interfaces/', plexpy.CONFIG.INTERFACE, 'images')
|
||||
self.icon = os.path.join(self.image_dir, 'logo-circle.ico')
|
||||
|
||||
if plexpy.UPDATE_AVAILABLE:
|
||||
self.icon = os.path.join(self.image_dir, 'logo-circle-update.ico')
|
||||
self.hover_text = common.PRODUCT + ' - Update Available!'
|
||||
self.update_title = 'Check for Updates - Update Available!'
|
||||
else:
|
||||
self.icon = os.path.join(self.image_dir, 'logo-circle.ico')
|
||||
self.hover_text = common.PRODUCT
|
||||
self.update_title = 'Check for Updates'
|
||||
|
||||
if plexpy.CONFIG.LAUNCH_STARTUP:
|
||||
launch_start_icon = os.path.join(self.image_dir, 'check-solid.ico')
|
||||
@@ -66,7 +67,7 @@ class WindowsSystemTray(object):
|
||||
['Start Tautulli at Login', launch_start_icon, self.tray_startup, None],
|
||||
['Open Browser when Tautulli Starts', launch_browser_icon, self.tray_browser, None],
|
||||
['', None, 'separator', None],
|
||||
['Check for Updates', None, self.tray_check_update, None],
|
||||
[self.update_title, None, self.tray_check_update, None],
|
||||
['Restart', None, self.tray_restart, None]
|
||||
]
|
||||
if not plexpy.FROZEN:
|
||||
@@ -105,8 +106,10 @@ class WindowsSystemTray(object):
|
||||
if plexpy.UPDATE_AVAILABLE:
|
||||
plexpy.SIGNAL = 'update'
|
||||
else:
|
||||
hover_text = common.PRODUCT + ' - No Update Available'
|
||||
self.update(hover_text=hover_text)
|
||||
self.hover_text = common.PRODUCT + ' - No Update Available'
|
||||
self.update_title = 'Check for Updates - No Update Available'
|
||||
self.menu[5][0] = self.update_title
|
||||
self.update(hover_text=self.hover_text, menu_options=self.menu)
|
||||
|
||||
def tray_restart(self, tray_icon):
|
||||
plexpy.SIGNAL = 'restart'
|
||||
@@ -116,12 +119,13 @@ class WindowsSystemTray(object):
|
||||
|
||||
def change_tray_update_icon(self):
|
||||
if plexpy.UPDATE_AVAILABLE:
|
||||
self.icon = os.path.join(self.image_dir, 'logo-circle-update.ico')
|
||||
self.hover_text = common.PRODUCT + ' - Update Available!'
|
||||
self.update_title = 'Check for Updates - Update Available!'
|
||||
else:
|
||||
self.icon = os.path.join(self.image_dir, 'logo-circle.ico')
|
||||
self.hover_text = common.PRODUCT + ' - No Update Available'
|
||||
self.update(icon=self.icon, hover_text=self.hover_text)
|
||||
self.update_title = 'Check for Updates'
|
||||
self.menu[5][0] = self.update_title
|
||||
self.update(hover_text=self.hover_text, menu_options=self.menu)
|
||||
|
||||
def change_tray_icons(self):
|
||||
if plexpy.CONFIG.LAUNCH_STARTUP:
|
||||
|
65
snap/snapcraft.yaml
Normal file
65
snap/snapcraft.yaml
Normal file
@@ -0,0 +1,65 @@
|
||||
name: tautulli
|
||||
adopt-info: tautulli
|
||||
summary: A Python based monitoring and tracking tool for Plex Media Server.
|
||||
description: >
|
||||
Tautulli is a 3rd party application that you can run alongside your Plex Media Server to monitor activity and track various statistics.
|
||||
Most importantly, these statistics include what has been watched, who watched it, when and where they watched it, and how it was watched.
|
||||
The only thing missing is "why they watched it", but who am I to question your 42 plays of Frozen.
|
||||
All statistics are presented in a nice and clean interface with many tables and graphs, which makes it easy to brag about your server to everyone else.
|
||||
|
||||
base: core18
|
||||
confinement: strict
|
||||
|
||||
parts:
|
||||
tautulli:
|
||||
plugin: dump
|
||||
source: .
|
||||
stage-packages:
|
||||
- python3
|
||||
- python3-openssl
|
||||
- python3-pycryptodome
|
||||
- python3-setuptools
|
||||
build-packages:
|
||||
- git
|
||||
override-pull: |
|
||||
snapcraftctl pull
|
||||
TAG_FULL=$(git describe --tag)
|
||||
TAG=$(echo $TAG_FULL | grep -oP '(v\d+\.\d+\.\d+(?>-beta)?)')
|
||||
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
COMMIT=$(git rev-parse HEAD)
|
||||
if [ "$TAG" = "$TAG_FULL" ]; then
|
||||
VERSION=$TAG
|
||||
else
|
||||
VERSION=$(echo $COMMIT | head -c 7)
|
||||
fi
|
||||
if [ ! "$VERSION" = "$TAG" ] || [ ! "$VERSION" = "${VERSION%-beta}" ]; then
|
||||
GRADE=devel
|
||||
else
|
||||
GRADE=stable
|
||||
fi
|
||||
if [ "$VERSION" = "$TAG" ] && [ ! "$VERSION" = "${VERSION%-beta}" ]; then
|
||||
BRANCH=beta
|
||||
elif [ "$VERSION" = "$TAG" ]; then
|
||||
BRANCH=master
|
||||
fi
|
||||
echo $BRANCH > branch.txt
|
||||
echo $COMMIT > version.txt
|
||||
snapcraftctl set-version "$VERSION"
|
||||
snapcraftctl set-grade "$GRADE"
|
||||
|
||||
apps:
|
||||
tautulli:
|
||||
command: >
|
||||
usr/bin/python3 $SNAP/Tautulli.py
|
||||
--datadir $SNAP_COMMON/Tautulli
|
||||
--config $SNAP_COMMON/Tautulli/config.ini
|
||||
--quiet
|
||||
--nolaunch
|
||||
daemon: simple
|
||||
restart-condition: on-abnormal
|
||||
restart-delay: 5s
|
||||
plugs:
|
||||
- network
|
||||
- network-bind
|
||||
environment:
|
||||
TAUTULLI_SNAP: "True"
|
Reference in New Issue
Block a user