Rename; Removed deps from repo
This commit is contained in:
@@ -1,11 +0,0 @@
|
|||||||
.git
|
|
||||||
.github
|
|
||||||
.gitignore
|
|
||||||
contrib
|
|
||||||
init-scripts
|
|
||||||
package
|
|
||||||
pylintrc
|
|
||||||
snap
|
|
||||||
*.md
|
|
||||||
!CHANGELOG*.md
|
|
||||||
start.bat
|
|
||||||
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
@@ -1,3 +0,0 @@
|
|||||||
github: JonnyWong16
|
|
||||||
patreon: Tautulli
|
|
||||||
custom: ["https://bit.ly/2InPp15", "https://bit.ly/2WTq83m"]
|
|
||||||
20
.github/pull_request_template.md
vendored
20
.github/pull_request_template.md
vendored
@@ -1,20 +0,0 @@
|
|||||||
## 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
|
|
||||||
115
.github/workflows/publish-docker.yml
vendored
115
.github/workflows/publish-docker.yml
vendored
@@ -1,115 +0,0 @@
|
|||||||
name: Publish Docker
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [master, beta, nightly]
|
|
||||||
tags: [v*]
|
|
||||||
pull_request: ~
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-docker:
|
|
||||||
name: Build Docker Image
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Prepare
|
|
||||||
id: prepare
|
|
||||||
run: |
|
|
||||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
|
||||||
echo ::set-output name=tag::${GITHUB_REF#refs/tags/}
|
|
||||||
elif [[ $GITHUB_REF == refs/heads/master ]]; then
|
|
||||||
echo ::set-output name=tag::latest
|
|
||||||
else
|
|
||||||
echo ::set-output name=tag::${GITHUB_REF#refs/heads/}
|
|
||||||
fi
|
|
||||||
if [[ $GITHUB_REF == refs/tags/*-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/}
|
|
||||||
fi
|
|
||||||
echo ::set-output name=commit::${GITHUB_SHA}
|
|
||||||
echo ::set-output name=build_date::$(date -u +'%Y-%m-%dT%H:%M:%SZ')
|
|
||||||
echo ::set-output name=docker_platforms::linux/amd64,linux/arm64/v8,linux/arm/v7,linux/arm/v6
|
|
||||||
echo ::set-output name=docker_image::${{ secrets.DOCKER_REPO }}/tautulli
|
|
||||||
|
|
||||||
- name: Set Up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v1
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v1
|
|
||||||
id: buildx
|
|
||||||
with:
|
|
||||||
version: latest
|
|
||||||
|
|
||||||
- name: Cache Docker Layers
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: /tmp/.buildx-cache
|
|
||||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-buildx-
|
|
||||||
|
|
||||||
- name: Login to DockerHub
|
|
||||||
uses: docker/login-action@v1
|
|
||||||
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() && github.event_name != 'pull_request'
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
password: ${{ secrets.GHCR_TOKEN }}
|
|
||||||
|
|
||||||
- name: Docker Build and Push
|
|
||||||
uses: docker/build-push-action@v2
|
|
||||||
if: success()
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: ./Dockerfile
|
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
|
||||||
platforms: ${{ steps.prepare.outputs.docker_platforms }}
|
|
||||||
build-args: |
|
|
||||||
TAG=${{ steps.prepare.outputs.tag }}
|
|
||||||
BRANCH=${{ steps.prepare.outputs.branch }}
|
|
||||||
COMMIT=${{ steps.prepare.outputs.commit }}
|
|
||||||
BUILD_DATE=${{ steps.prepare.outputs.build_date }}
|
|
||||||
tags: |
|
|
||||||
${{ steps.prepare.outputs.docker_image }}:${{ steps.prepare.outputs.tag }}
|
|
||||||
ghcr.io/${{ steps.prepare.outputs.docker_image }}:${{ steps.prepare.outputs.tag }}
|
|
||||||
cache-from: type=local,src=/tmp/.buildx-cache
|
|
||||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
|
||||||
|
|
||||||
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
|
|
||||||
with:
|
|
||||||
webhook: ${{ secrets.DISCORD_WEBHOOK }}
|
|
||||||
status: ${{ steps.status.outputs.status }}
|
|
||||||
title: ${{ github.workflow }}
|
|
||||||
nofail: true
|
|
||||||
194
.github/workflows/publish-installers.yml
vendored
194
.github/workflows/publish-installers.yml
vendored
@@ -1,194 +0,0 @@
|
|||||||
name: Publish Installers
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [master, beta, nightly]
|
|
||||||
tags: [v*]
|
|
||||||
pull_request: ~
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
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
|
|
||||||
|
|
||||||
- name: Set Release Version
|
|
||||||
id: get_version
|
|
||||||
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}
|
|
||||||
fi
|
|
||||||
if [[ $GITHUB_REF == refs/tags/*-beta ]]; then
|
|
||||||
echo "beta" > branch.txt
|
|
||||||
elif [[ $GITHUB_REF == refs/tags/* ]]; then
|
|
||||||
echo "master" > branch.txt
|
|
||||||
else
|
|
||||||
echo ${GITHUB_REF#refs/heads/} > branch.txt
|
|
||||||
fi
|
|
||||||
echo $GITHUB_SHA > version.txt
|
|
||||||
|
|
||||||
- name: Set Up Python
|
|
||||||
uses: actions/setup-python@v2
|
|
||||||
with:
|
|
||||||
python-version: 3.8
|
|
||||||
|
|
||||||
- name: Cache Dependencies
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ~\AppData\Local\pip\Cache
|
|
||||||
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-${{ matrix.os }}.txt
|
|
||||||
|
|
||||||
- name: Build Package
|
|
||||||
run: |
|
|
||||||
pyinstaller -y ./package/Tautulli-${{ matrix.os }}.spec
|
|
||||||
|
|
||||||
- name: Move Windows Updater Files
|
|
||||||
if: matrix.os == 'windows'
|
|
||||||
run: |
|
|
||||||
Move-Item dist\updater\* dist\Tautulli\ -Force
|
|
||||||
|
|
||||||
- name: Create Windows Installer
|
|
||||||
uses: joncloud/makensis-action@v3.4
|
|
||||||
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
|
|
||||||
additional-plugin-paths: package/nsis-plugins
|
|
||||||
|
|
||||||
- name: Create MacOS Installer
|
|
||||||
if: matrix.os == 'macos'
|
|
||||||
run: |
|
|
||||||
sudo pkgbuild \
|
|
||||||
--install-location /Applications \
|
|
||||||
--version ${{ steps.get_version.outputs.VERSION }} \
|
|
||||||
--component ./dist/Tautulli.app \
|
|
||||||
--scripts ./package/macos-scripts \
|
|
||||||
Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.pkg
|
|
||||||
|
|
||||||
- name: Upload Installer
|
|
||||||
uses: actions/upload-artifact@v2
|
|
||||||
with:
|
|
||||||
name: Tautulli-${{ matrix.os }}-installer
|
|
||||||
path: Tautulli-${{ matrix.os }}-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.${{ matrix.ext }}
|
|
||||||
|
|
||||||
release:
|
|
||||||
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
|
|
||||||
uses: technote-space/workflow-conclusion-action@v1
|
|
||||||
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Set Release Version
|
|
||||||
id: get_version
|
|
||||||
run: |
|
|
||||||
echo ::set-output name=RELEASE_VERSION::${GITHUB_REF#refs/tags/}
|
|
||||||
|
|
||||||
- name: Download Installers
|
|
||||||
if: env.WORKFLOW_CONCLUSION == 'success'
|
|
||||||
uses: actions/download-artifact@v2
|
|
||||||
|
|
||||||
- name: Get Changelog
|
|
||||||
id: get_changelog
|
|
||||||
run: |
|
|
||||||
echo ::set-output name=CHANGELOG::"$( sed -n '/^## /{p; :loop n; p; /^## /q; b loop}' CHANGELOG.md \
|
|
||||||
| sed '$d' | sed '$d' | sed '$d' | sed ':a;N;$!ba;s/\n/%0A/g' )"
|
|
||||||
|
|
||||||
- name: Create Release
|
|
||||||
uses: actions/create-release@v1
|
|
||||||
id: create_release
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
tag_name: ${{ steps.get_version.outputs.RELEASE_VERSION }}
|
|
||||||
release_name: Tautulli ${{ steps.get_version.outputs.RELEASE_VERSION }}
|
|
||||||
body: |
|
|
||||||
## Changelog
|
|
||||||
|
|
||||||
##${{ steps.get_changelog.outputs.CHANGELOG }}
|
|
||||||
draft: false
|
|
||||||
prerelease: ${{ endsWith(steps.get_version.outputs.RELEASE_VERSION, '-beta') }}
|
|
||||||
|
|
||||||
- name: Upload Windows Installer
|
|
||||||
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-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
|
|
||||||
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-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
94
.github/workflows/publish-snap.yml
vendored
@@ -1,94 +0,0 @@
|
|||||||
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
|
|
||||||
28
.github/workflows/pull-requests.yml
vendored
28
.github/workflows/pull-requests.yml
vendored
@@ -1,28 +0,0 @@
|
|||||||
name: Pull Requests
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types: [opened, synchronize, edited, reopened]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check-branch:
|
|
||||||
name: Check Pull Request
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Comment on Pull Request
|
|
||||||
uses: mshick/add-pr-comment@v1
|
|
||||||
if: github.base_ref != 'nightly'
|
|
||||||
with:
|
|
||||||
message: Pull requests must be made to the `nightly` branch. Thanks.
|
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
repo-token-user-login: 'github-actions[bot]'
|
|
||||||
|
|
||||||
- name: Fail Workflow
|
|
||||||
if: github.base_ref != 'nightly'
|
|
||||||
run: |
|
|
||||||
echo Base: ${{ github.base_ref }}
|
|
||||||
echo Head: ${{ github.head_ref }}
|
|
||||||
exit 1
|
|
||||||
343
.gitignore
vendored
343
.gitignore
vendored
@@ -1,92 +1,275 @@
|
|||||||
|
|
||||||
# Compiled source #
|
# Created by https://www.toptal.com/developers/gitignore/api/pycharm+all,python,linux,windows
|
||||||
###################
|
# Edit at https://www.toptal.com/developers/gitignore?templates=pycharm+all,python,linux,windows
|
||||||
__pycache__
|
|
||||||
*.pyc
|
|
||||||
*.py~
|
|
||||||
*.pyproj
|
|
||||||
*.sln
|
|
||||||
|
|
||||||
# PlexPy files #
|
### Linux ###
|
||||||
######################
|
*~
|
||||||
|
|
||||||
|
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||||
|
.fuse_hidden*
|
||||||
|
|
||||||
|
# KDE directory preferences
|
||||||
|
.directory
|
||||||
|
|
||||||
|
# Linux trash folder which might appear on any partition or disk
|
||||||
|
.Trash-*
|
||||||
|
|
||||||
|
# .nfs files are created when an open file is removed but is still being accessed
|
||||||
|
.nfs*
|
||||||
|
|
||||||
|
### PyCharm+all ###
|
||||||
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||||
|
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||||
|
|
||||||
|
# User-specific stuff
|
||||||
|
.idea/**/workspace.xml
|
||||||
|
.idea/**/tasks.xml
|
||||||
|
.idea/**/usage.statistics.xml
|
||||||
|
.idea/**/dictionaries
|
||||||
|
.idea/**/shelf
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
.idea/**/contentModel.xml
|
||||||
|
|
||||||
|
# Sensitive or high-churn files
|
||||||
|
.idea/**/dataSources/
|
||||||
|
.idea/**/dataSources.ids
|
||||||
|
.idea/**/dataSources.local.xml
|
||||||
|
.idea/**/sqlDataSources.xml
|
||||||
|
.idea/**/dynamic.xml
|
||||||
|
.idea/**/uiDesigner.xml
|
||||||
|
.idea/**/dbnavigator.xml
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
.idea/**/gradle.xml
|
||||||
|
.idea/**/libraries
|
||||||
|
|
||||||
|
# Gradle and Maven with auto-import
|
||||||
|
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||||
|
# since they will be recreated, and may cause churn. Uncomment if using
|
||||||
|
# auto-import.
|
||||||
|
# .idea/artifacts
|
||||||
|
# .idea/compiler.xml
|
||||||
|
# .idea/jarRepositories.xml
|
||||||
|
# .idea/modules.xml
|
||||||
|
# .idea/*.iml
|
||||||
|
# .idea/modules
|
||||||
|
# *.iml
|
||||||
|
# *.ipr
|
||||||
|
|
||||||
|
# CMake
|
||||||
|
cmake-build-*/
|
||||||
|
|
||||||
|
# Mongo Explorer plugin
|
||||||
|
.idea/**/mongoSettings.xml
|
||||||
|
|
||||||
|
# File-based project format
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
out/
|
||||||
|
|
||||||
|
# mpeltonen/sbt-idea plugin
|
||||||
|
.idea_modules/
|
||||||
|
|
||||||
|
# JIRA plugin
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
|
||||||
|
# Cursive Clojure plugin
|
||||||
|
.idea/replstate.xml
|
||||||
|
|
||||||
|
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||||
|
com_crashlytics_export_strings.xml
|
||||||
|
crashlytics.properties
|
||||||
|
crashlytics-build.properties
|
||||||
|
fabric.properties
|
||||||
|
|
||||||
|
# Editor-based Rest Client
|
||||||
|
.idea/httpRequests
|
||||||
|
|
||||||
|
# Android studio 3.1+ serialized cache file
|
||||||
|
.idea/caches/build_file_checksums.ser
|
||||||
|
|
||||||
|
### PyCharm+all Patch ###
|
||||||
|
# Ignores the whole .idea folder and all .iml files
|
||||||
|
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
|
||||||
|
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
|
||||||
|
|
||||||
|
*.iml
|
||||||
|
modules.xml
|
||||||
|
.idea/misc.xml
|
||||||
|
*.ipr
|
||||||
|
|
||||||
|
# Sonarlint plugin
|
||||||
|
.idea/sonarlint
|
||||||
|
|
||||||
|
### Python ###
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
pip-wheel-metadata/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
pytestdebug.log
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
*.log
|
*.log
|
||||||
*.db*
|
local_settings.py
|
||||||
*.db-journal
|
db.sqlite3
|
||||||
*.ini
|
db.sqlite3-journal
|
||||||
release.lock
|
|
||||||
version.lock
|
|
||||||
logs/*
|
|
||||||
backups/*
|
|
||||||
cache/*
|
|
||||||
exports/*
|
|
||||||
newsletters/*
|
|
||||||
*.mmdb
|
|
||||||
version.txt
|
|
||||||
branch.txt
|
|
||||||
|
|
||||||
# HTTPS Cert/Key #
|
# Flask stuff:
|
||||||
##################
|
instance/
|
||||||
/*.crt
|
.webassets-cache
|
||||||
/*.key
|
|
||||||
/*.csr
|
|
||||||
/*.pem
|
|
||||||
|
|
||||||
# Mergetool
|
# Scrapy stuff:
|
||||||
*.orgin
|
.scrapy
|
||||||
|
|
||||||
# OS generated files #
|
# Sphinx documentation
|
||||||
######################
|
docs/_build/
|
||||||
.DS_Store?
|
doc/_build/
|
||||||
.DS_Store
|
|
||||||
ehthumbs.db
|
# PyBuilder
|
||||||
Icon?
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
pythonenv*
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# profiling data
|
||||||
|
.prof
|
||||||
|
|
||||||
|
### Windows ###
|
||||||
|
# Windows thumbnail cache files
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
Thumbs.db:encryptable
|
||||||
|
ehthumbs.db
|
||||||
|
ehthumbs_vista.db
|
||||||
|
|
||||||
#Ignore files generated by PyCharm
|
# Dump file
|
||||||
*.idea/*
|
*.stackdump
|
||||||
|
|
||||||
#Ignore files generated by vi
|
# Folder config file
|
||||||
*.swp
|
[Dd]esktop.ini
|
||||||
|
|
||||||
#Ignore files build by Visual Studio
|
# Recycle Bin used on file shares
|
||||||
*.obj
|
$RECYCLE.BIN/
|
||||||
*.exe
|
|
||||||
*.pdb
|
|
||||||
*.user
|
|
||||||
*.aps
|
|
||||||
*.pch
|
|
||||||
*.vspscc
|
|
||||||
*_i.c
|
|
||||||
*_p.c
|
|
||||||
*.ncb
|
|
||||||
*.suo
|
|
||||||
*.tlb
|
|
||||||
*.tlh
|
|
||||||
*.bak
|
|
||||||
*.cache
|
|
||||||
*.ilk
|
|
||||||
[Bb]in
|
|
||||||
[Dd]ebug*/
|
|
||||||
*.lib
|
|
||||||
*.sbr
|
|
||||||
obj/
|
|
||||||
[Rr]elease*/
|
|
||||||
_ReSharper*/
|
|
||||||
[Tt]est[Rr]esult*
|
|
||||||
/cache
|
|
||||||
/logs
|
|
||||||
.project
|
|
||||||
.pydevproject
|
|
||||||
|
|
||||||
#Ignore files generated by pyinstaller
|
# Windows Installer files
|
||||||
/build
|
*.cab
|
||||||
/dist
|
*.msi
|
||||||
|
*.msix
|
||||||
|
*.msm
|
||||||
|
*.msp
|
||||||
|
|
||||||
#snapcraft specifics
|
# Windows shortcuts
|
||||||
/parts/
|
*.lnk
|
||||||
/stage/
|
|
||||||
/prime/
|
# End of https://www.toptal.com/developers/gitignore/api/pycharm+all,python,linux,windows
|
||||||
*.snap
|
|
||||||
.snapcraft
|
|
||||||
*_source.tar.bz2
|
|
||||||
snap/.snapcraft
|
|
||||||
|
|||||||
26
Dockerfile
26
Dockerfile
@@ -1,26 +0,0 @@
|
|||||||
FROM tautulli/tautulli-baseimage:python3
|
|
||||||
|
|
||||||
LABEL maintainer="Tautulli"
|
|
||||||
|
|
||||||
ARG BRANCH
|
|
||||||
ARG COMMIT
|
|
||||||
|
|
||||||
ENV TAUTULLI_DOCKER=True
|
|
||||||
ENV TZ=UTC
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
RUN \
|
|
||||||
groupadd -g 1000 tautulli && \
|
|
||||||
useradd -u 1000 -g 1000 tautulli && \
|
|
||||||
echo ${BRANCH} > /app/branch.txt && \
|
|
||||||
echo ${COMMIT} > /app/version.txt
|
|
||||||
|
|
||||||
COPY . /app
|
|
||||||
|
|
||||||
CMD [ "python", "Tautulli.py", "--datadir", "/config" ]
|
|
||||||
ENTRYPOINT [ "./start.sh" ]
|
|
||||||
|
|
||||||
VOLUME /config
|
|
||||||
EXPOSE 8181
|
|
||||||
HEALTHCHECK --start-period=90s CMD curl -ILfSs http://localhost:8181/status > /dev/null || curl -ILfkSs https://localhost:8181/status > /dev/null || exit 1
|
|
||||||
@@ -20,11 +20,6 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
# Ensure lib added to path, before any other imports
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'lib'))
|
|
||||||
|
|
||||||
from future.builtins import str
|
|
||||||
|
|
||||||
import appdirs
|
import appdirs
|
||||||
import argparse
|
import argparse
|
||||||
import datetime
|
import datetime
|
||||||
@@ -36,16 +31,16 @@ import time
|
|||||||
import threading
|
import threading
|
||||||
import tzlocal
|
import tzlocal
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
from plexpy import common, config, database, helpers, logger, webstart
|
from jellypy import common, config, database, helpers, logger, webstart
|
||||||
if common.PLATFORM == 'Windows':
|
if common.PLATFORM == 'Windows':
|
||||||
from plexpy import windows
|
from jellypy import windows
|
||||||
elif common.PLATFORM == 'Darwin':
|
elif common.PLATFORM == 'Darwin':
|
||||||
from plexpy import macos
|
from jellypy import macos
|
||||||
|
|
||||||
# Register signals, such as CTRL + C
|
# Register signals, such as CTRL + C
|
||||||
signal.signal(signal.SIGINT, plexpy.sig_handler)
|
signal.signal(signal.SIGINT, jellypy.sig_handler)
|
||||||
signal.signal(signal.SIGTERM, plexpy.sig_handler)
|
signal.signal(signal.SIGTERM, jellypy.sig_handler)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@@ -56,28 +51,28 @@ def main():
|
|||||||
|
|
||||||
# Fixed paths to Tautulli
|
# Fixed paths to Tautulli
|
||||||
if hasattr(sys, 'frozen') and hasattr(sys, '_MEIPASS'):
|
if hasattr(sys, 'frozen') and hasattr(sys, '_MEIPASS'):
|
||||||
plexpy.FROZEN = True
|
jellypy.FROZEN = True
|
||||||
plexpy.FULL_PATH = os.path.abspath(sys.executable)
|
jellypy.FULL_PATH = os.path.abspath(sys.executable)
|
||||||
plexpy.PROG_DIR = sys._MEIPASS
|
jellypy.PROG_DIR = sys._MEIPASS
|
||||||
else:
|
else:
|
||||||
plexpy.FULL_PATH = os.path.abspath(__file__)
|
jellypy.FULL_PATH = os.path.abspath(__file__)
|
||||||
plexpy.PROG_DIR = os.path.dirname(plexpy.FULL_PATH)
|
jellypy.PROG_DIR = os.path.dirname(jellypy.FULL_PATH)
|
||||||
|
|
||||||
plexpy.ARGS = sys.argv[1:]
|
jellypy.ARGS = sys.argv[1:]
|
||||||
|
|
||||||
# From sickbeard
|
# From sickbeard
|
||||||
plexpy.SYS_PLATFORM = sys.platform
|
jellypy.SYS_PLATFORM = sys.platform
|
||||||
plexpy.SYS_ENCODING = None
|
jellypy.SYS_ENCODING = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
locale.setlocale(locale.LC_ALL, "")
|
locale.setlocale(locale.LC_ALL, "")
|
||||||
plexpy.SYS_LANGUAGE, plexpy.SYS_ENCODING = locale.getdefaultlocale()
|
jellypy.SYS_LANGUAGE, jellypy.SYS_ENCODING = locale.getdefaultlocale()
|
||||||
except (locale.Error, IOError):
|
except (locale.Error, IOError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# for OSes that are poorly configured I'll just force UTF-8
|
# for OSes that are poorly configured I'll just force UTF-8
|
||||||
if not plexpy.SYS_ENCODING or plexpy.SYS_ENCODING in ('ANSI_X3.4-1968', 'US-ASCII', 'ASCII'):
|
if not jellypy.SYS_ENCODING or jellypy.SYS_ENCODING in ('ANSI_X3.4-1968', 'US-ASCII', 'ASCII'):
|
||||||
plexpy.SYS_ENCODING = 'UTF-8'
|
jellypy.SYS_ENCODING = 'UTF-8'
|
||||||
|
|
||||||
# Set up and gather command line arguments
|
# Set up and gather command line arguments
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
@@ -107,50 +102,50 @@ def main():
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if args.verbose:
|
if args.verbose:
|
||||||
plexpy.VERBOSE = True
|
jellypy.VERBOSE = True
|
||||||
if args.quiet:
|
if args.quiet:
|
||||||
plexpy.QUIET = True
|
jellypy.QUIET = True
|
||||||
|
|
||||||
# Do an intial setup of the logger.
|
# Do an intial setup of the logger.
|
||||||
# Require verbose for pre-initilization to see critical errors
|
# Require verbose for pre-initilization to see critical errors
|
||||||
logger.initLogger(console=not plexpy.QUIET, log_dir=False, verbose=True)
|
logger.initLogger(console=not jellypy.QUIET, log_dir=False, verbose=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
plexpy.SYS_TIMEZONE = tzlocal.get_localzone()
|
jellypy.SYS_TIMEZONE = tzlocal.get_localzone()
|
||||||
except (pytz.UnknownTimeZoneError, LookupError, ValueError) as e:
|
except (pytz.UnknownTimeZoneError, LookupError, ValueError) as e:
|
||||||
logger.error("Could not determine system timezone: %s" % e)
|
logger.error("Could not determine system timezone: %s" % e)
|
||||||
plexpy.SYS_TIMEZONE = pytz.UTC
|
jellypy.SYS_TIMEZONE = pytz.UTC
|
||||||
|
|
||||||
plexpy.SYS_UTC_OFFSET = datetime.datetime.now(plexpy.SYS_TIMEZONE).strftime('%z')
|
jellypy.SYS_UTC_OFFSET = datetime.datetime.now(jellypy.SYS_TIMEZONE).strftime('%z')
|
||||||
|
|
||||||
if helpers.bool_true(os.getenv('TAUTULLI_DOCKER', False)):
|
if helpers.bool_true(os.getenv('TAUTULLI_DOCKER', False)):
|
||||||
plexpy.DOCKER = True
|
jellypy.DOCKER = True
|
||||||
if helpers.bool_true(os.getenv('TAUTULLI_SNAP', False)):
|
if helpers.bool_true(os.getenv('TAUTULLI_SNAP', False)):
|
||||||
plexpy.SNAP = True
|
jellypy.SNAP = True
|
||||||
|
|
||||||
if args.dev:
|
if args.dev:
|
||||||
plexpy.DEV = True
|
jellypy.DEV = True
|
||||||
logger.debug("Tautulli is running in the dev environment.")
|
logger.debug("Tautulli is running in the dev environment.")
|
||||||
|
|
||||||
if args.daemon:
|
if args.daemon:
|
||||||
if sys.platform == 'win32':
|
if sys.platform == 'win32':
|
||||||
logger.warn("Daemonizing not supported under Windows, starting normally")
|
logger.warn("Daemonizing not supported under Windows, starting normally")
|
||||||
else:
|
else:
|
||||||
plexpy.DAEMON = True
|
jellypy.DAEMON = True
|
||||||
plexpy.QUIET = True
|
jellypy.QUIET = True
|
||||||
|
|
||||||
if args.nofork:
|
if args.nofork:
|
||||||
plexpy.NOFORK = True
|
jellypy.NOFORK = True
|
||||||
logger.info("Tautulli is running as a service, it will not fork when restarted.")
|
logger.info("Tautulli is running as a service, it will not fork when restarted.")
|
||||||
|
|
||||||
if args.pidfile:
|
if args.pidfile:
|
||||||
plexpy.PIDFILE = str(args.pidfile)
|
jellypy.PIDFILE = str(args.pidfile)
|
||||||
|
|
||||||
# If the pidfile already exists, plexpy may still be running, so
|
# If the pidfile already exists, jellypy may still be running, so
|
||||||
# exit
|
# exit
|
||||||
if os.path.exists(plexpy.PIDFILE):
|
if os.path.exists(jellypy.PIDFILE):
|
||||||
try:
|
try:
|
||||||
with open(plexpy.PIDFILE, 'r') as fp:
|
with open(jellypy.PIDFILE, 'r') as fp:
|
||||||
pid = int(fp.read())
|
pid = int(fp.read())
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
raise SystemExit("Unable to read PID file: %s", e)
|
raise SystemExit("Unable to read PID file: %s", e)
|
||||||
@@ -160,20 +155,20 @@ def main():
|
|||||||
except OSError:
|
except OSError:
|
||||||
logger.warn("PID file '%s' already exists, but PID %d is "
|
logger.warn("PID file '%s' already exists, but PID %d is "
|
||||||
"not running. Ignoring PID file." %
|
"not running. Ignoring PID file." %
|
||||||
(plexpy.PIDFILE, pid))
|
(jellypy.PIDFILE, pid))
|
||||||
else:
|
else:
|
||||||
# The pidfile exists and points to a live PID. plexpy may
|
# The pidfile exists and points to a live PID. jellypy may
|
||||||
# still be running, so exit.
|
# still be running, so exit.
|
||||||
raise SystemExit("PID file '%s' already exists. Exiting." %
|
raise SystemExit("PID file '%s' already exists. Exiting." %
|
||||||
plexpy.PIDFILE)
|
jellypy.PIDFILE)
|
||||||
|
|
||||||
# The pidfile is only useful in daemon mode, make sure we can write the
|
# The pidfile is only useful in daemon mode, make sure we can write the
|
||||||
# file properly
|
# file properly
|
||||||
if plexpy.DAEMON:
|
if jellypy.DAEMON:
|
||||||
plexpy.CREATEPID = True
|
jellypy.CREATEPID = True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(plexpy.PIDFILE, 'w') as fp:
|
with open(jellypy.PIDFILE, 'w') as fp:
|
||||||
fp.write("pid\n")
|
fp.write("pid\n")
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
raise SystemExit("Unable to write PID file: %s", e)
|
raise SystemExit("Unable to write PID file: %s", e)
|
||||||
@@ -183,107 +178,107 @@ def main():
|
|||||||
|
|
||||||
# Determine which data directory and config file to use
|
# Determine which data directory and config file to use
|
||||||
if args.datadir:
|
if args.datadir:
|
||||||
plexpy.DATA_DIR = args.datadir
|
jellypy.DATA_DIR = args.datadir
|
||||||
elif plexpy.FROZEN:
|
elif jellypy.FROZEN:
|
||||||
plexpy.DATA_DIR = appdirs.user_data_dir("Tautulli", False)
|
jellypy.DATA_DIR = appdirs.user_data_dir("Tautulli", False)
|
||||||
else:
|
else:
|
||||||
plexpy.DATA_DIR = plexpy.PROG_DIR
|
jellypy.DATA_DIR = jellypy.PROG_DIR
|
||||||
|
|
||||||
# Migrate Snap data dir
|
# Migrate Snap data dir
|
||||||
if plexpy.SNAP:
|
if jellypy.SNAP:
|
||||||
snap_common = os.environ['SNAP_COMMON']
|
snap_common = os.environ['SNAP_COMMON']
|
||||||
old_data_dir = os.path.join(snap_common, 'Tautulli')
|
old_data_dir = os.path.join(snap_common, 'Tautulli')
|
||||||
if os.path.exists(old_data_dir) and os.listdir(old_data_dir):
|
if os.path.exists(old_data_dir) and os.listdir(old_data_dir):
|
||||||
plexpy.SNAP_MIGRATE = True
|
jellypy.SNAP_MIGRATE = True
|
||||||
logger.info("Migrating Snap user data.")
|
logger.info("Migrating Snap user data.")
|
||||||
shutil.move(old_data_dir, plexpy.DATA_DIR)
|
shutil.move(old_data_dir, jellypy.DATA_DIR)
|
||||||
|
|
||||||
if args.config:
|
if args.config:
|
||||||
config_file = args.config
|
config_file = args.config
|
||||||
else:
|
else:
|
||||||
config_file = os.path.join(plexpy.DATA_DIR, config.FILENAME)
|
config_file = os.path.join(jellypy.DATA_DIR, config.FILENAME)
|
||||||
|
|
||||||
# Try to create the DATA_DIR if it doesn't exist
|
# Try to create the DATA_DIR if it doesn't exist
|
||||||
if not os.path.exists(plexpy.DATA_DIR):
|
if not os.path.exists(jellypy.DATA_DIR):
|
||||||
try:
|
try:
|
||||||
os.makedirs(plexpy.DATA_DIR)
|
os.makedirs(jellypy.DATA_DIR)
|
||||||
except OSError:
|
except OSError:
|
||||||
raise SystemExit(
|
raise SystemExit(
|
||||||
'Could not create data directory: ' + plexpy.DATA_DIR + '. Exiting....')
|
'Could not create data directory: ' + jellypy.DATA_DIR + '. Exiting....')
|
||||||
|
|
||||||
# Make sure the DATA_DIR is writeable
|
# Make sure the DATA_DIR is writeable
|
||||||
if not os.access(plexpy.DATA_DIR, os.W_OK):
|
if not os.access(jellypy.DATA_DIR, os.W_OK):
|
||||||
raise SystemExit(
|
raise SystemExit(
|
||||||
'Cannot write to the data directory: ' + plexpy.DATA_DIR + '. Exiting...')
|
'Cannot write to the data directory: ' + jellypy.DATA_DIR + '. Exiting...')
|
||||||
|
|
||||||
# Put the database in the DATA_DIR
|
# Put the database in the DATA_DIR
|
||||||
plexpy.DB_FILE = os.path.join(plexpy.DATA_DIR, database.FILENAME)
|
jellypy.DB_FILE = os.path.join(jellypy.DATA_DIR, database.FILENAME)
|
||||||
|
|
||||||
# Move 'plexpy.db' to 'tautulli.db'
|
# Move 'jellypy.db' to 'tautulli.db'
|
||||||
if os.path.isfile(os.path.join(plexpy.DATA_DIR, 'plexpy.db')) and \
|
if os.path.isfile(os.path.join(jellypy.DATA_DIR, 'jellypy.db')) and \
|
||||||
not os.path.isfile(os.path.join(plexpy.DATA_DIR, plexpy.DB_FILE)):
|
not os.path.isfile(os.path.join(jellypy.DATA_DIR, jellypy.DB_FILE)):
|
||||||
try:
|
try:
|
||||||
os.rename(os.path.join(plexpy.DATA_DIR, 'plexpy.db'), plexpy.DB_FILE)
|
os.rename(os.path.join(jellypy.DATA_DIR, 'jellypy.db'), jellypy.DB_FILE)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
raise SystemExit("Unable to rename plexpy.db to tautulli.db: %s", e)
|
raise SystemExit("Unable to rename jellypy.db to tautulli.db: %s", e)
|
||||||
|
|
||||||
if plexpy.DAEMON:
|
if jellypy.DAEMON:
|
||||||
plexpy.daemonize()
|
jellypy.daemonize()
|
||||||
|
|
||||||
# Read config and start logging
|
# Read config and start logging
|
||||||
plexpy.initialize(config_file)
|
jellypy.initialize(config_file)
|
||||||
|
|
||||||
# Start the background threads
|
# Start the background threads
|
||||||
plexpy.start()
|
jellypy.start()
|
||||||
|
|
||||||
# Force the http port if neccessary
|
# Force the http port if neccessary
|
||||||
if args.port:
|
if args.port:
|
||||||
plexpy.HTTP_PORT = args.port
|
jellypy.HTTP_PORT = args.port
|
||||||
logger.info('Using forced web server port: %i', plexpy.HTTP_PORT)
|
logger.info('Using forced web server port: %i', jellypy.HTTP_PORT)
|
||||||
else:
|
else:
|
||||||
plexpy.HTTP_PORT = int(plexpy.CONFIG.HTTP_PORT)
|
jellypy.HTTP_PORT = int(jellypy.CONFIG.HTTP_PORT)
|
||||||
|
|
||||||
# Check if pyOpenSSL is installed. It is required for certificate generation
|
# Check if pyOpenSSL is installed. It is required for certificate generation
|
||||||
# and for CherryPy.
|
# and for CherryPy.
|
||||||
if plexpy.CONFIG.ENABLE_HTTPS:
|
if jellypy.CONFIG.ENABLE_HTTPS:
|
||||||
try:
|
try:
|
||||||
import OpenSSL
|
import OpenSSL
|
||||||
except ImportError:
|
except ImportError:
|
||||||
logger.warn("The pyOpenSSL module is missing. Install this "
|
logger.warn("The pyOpenSSL module is missing. Install this "
|
||||||
"module to enable HTTPS. HTTPS will be disabled.")
|
"module to enable HTTPS. HTTPS will be disabled.")
|
||||||
plexpy.CONFIG.ENABLE_HTTPS = False
|
jellypy.CONFIG.ENABLE_HTTPS = False
|
||||||
|
|
||||||
# Try to start the server. Will exit here is address is already in use.
|
# Try to start the server. Will exit here is address is already in use.
|
||||||
webstart.start()
|
webstart.start()
|
||||||
|
|
||||||
if common.PLATFORM == 'Windows':
|
if common.PLATFORM == 'Windows':
|
||||||
if plexpy.CONFIG.SYS_TRAY_ICON:
|
if jellypy.CONFIG.SYS_TRAY_ICON:
|
||||||
plexpy.WIN_SYS_TRAY_ICON = windows.WindowsSystemTray()
|
jellypy.WIN_SYS_TRAY_ICON = windows.WindowsSystemTray()
|
||||||
plexpy.WIN_SYS_TRAY_ICON.start()
|
jellypy.WIN_SYS_TRAY_ICON.start()
|
||||||
windows.set_startup()
|
windows.set_startup()
|
||||||
elif common.PLATFORM == 'Darwin':
|
elif common.PLATFORM == 'Darwin':
|
||||||
macos.set_startup()
|
macos.set_startup()
|
||||||
|
|
||||||
# Open webbrowser
|
# Open webbrowser
|
||||||
if plexpy.CONFIG.LAUNCH_BROWSER and not args.nolaunch and not plexpy.DEV:
|
if jellypy.CONFIG.LAUNCH_BROWSER and not args.nolaunch and not jellypy.DEV:
|
||||||
plexpy.launch_browser(plexpy.CONFIG.HTTP_HOST, plexpy.HTTP_PORT,
|
jellypy.launch_browser(jellypy.CONFIG.HTTP_HOST, jellypy.HTTP_PORT,
|
||||||
plexpy.HTTP_ROOT)
|
jellypy.HTTP_ROOT)
|
||||||
|
|
||||||
if common.PLATFORM == 'Darwin' and plexpy.CONFIG.SYS_TRAY_ICON:
|
if common.PLATFORM == 'Darwin' and jellypy.CONFIG.SYS_TRAY_ICON:
|
||||||
if not macos.HAS_PYOBJC:
|
if not macos.HAS_PYOBJC:
|
||||||
logger.warn("The pyobjc module is missing. Install this "
|
logger.warn("The pyobjc module is missing. Install this "
|
||||||
"module to enable the MacOS menu bar icon.")
|
"module to enable the MacOS menu bar icon.")
|
||||||
plexpy.CONFIG.SYS_TRAY_ICON = False
|
jellypy.CONFIG.SYS_TRAY_ICON = False
|
||||||
|
|
||||||
if plexpy.CONFIG.SYS_TRAY_ICON:
|
if jellypy.CONFIG.SYS_TRAY_ICON:
|
||||||
# MacOS menu bar icon must be run on the main thread and is blocking
|
# MacOS menu bar icon must be run on the main thread and is blocking
|
||||||
# Start the rest of Tautulli on a new thread
|
# Start the rest of Tautulli on a new thread
|
||||||
thread = threading.Thread(target=wait)
|
thread = threading.Thread(target=wait)
|
||||||
thread.daemon = True
|
thread.daemon = True
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
plexpy.MAC_SYS_TRAY_ICON = macos.MacOSSystemTray()
|
jellypy.MAC_SYS_TRAY_ICON = macos.MacOSSystemTray()
|
||||||
plexpy.MAC_SYS_TRAY_ICON.start()
|
jellypy.MAC_SYS_TRAY_ICON.start()
|
||||||
else:
|
else:
|
||||||
wait()
|
wait()
|
||||||
else:
|
else:
|
||||||
@@ -295,29 +290,29 @@ def wait():
|
|||||||
|
|
||||||
# Wait endlessly for a signal to happen
|
# Wait endlessly for a signal to happen
|
||||||
while True:
|
while True:
|
||||||
if not plexpy.SIGNAL:
|
if not jellypy.SIGNAL:
|
||||||
try:
|
try:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
plexpy.SIGNAL = 'shutdown'
|
jellypy.SIGNAL = 'shutdown'
|
||||||
else:
|
else:
|
||||||
logger.info('Received signal: %s', plexpy.SIGNAL)
|
logger.info('Received signal: %s', jellypy.SIGNAL)
|
||||||
|
|
||||||
if plexpy.SIGNAL == 'shutdown':
|
if jellypy.SIGNAL == 'shutdown':
|
||||||
plexpy.shutdown()
|
jellypy.shutdown()
|
||||||
elif plexpy.SIGNAL == 'restart':
|
elif jellypy.SIGNAL == 'restart':
|
||||||
plexpy.shutdown(restart=True)
|
jellypy.shutdown(restart=True)
|
||||||
elif plexpy.SIGNAL == 'checkout':
|
elif jellypy.SIGNAL == 'checkout':
|
||||||
plexpy.shutdown(restart=True, checkout=True)
|
jellypy.shutdown(restart=True, checkout=True)
|
||||||
elif plexpy.SIGNAL == 'reset':
|
elif jellypy.SIGNAL == 'reset':
|
||||||
plexpy.shutdown(restart=True, reset=True)
|
jellypy.shutdown(restart=True, reset=True)
|
||||||
elif plexpy.SIGNAL == 'update':
|
elif jellypy.SIGNAL == 'update':
|
||||||
plexpy.shutdown(restart=True, update=True)
|
jellypy.shutdown(restart=True, update=True)
|
||||||
else:
|
else:
|
||||||
logger.error('Unknown signal. Shutting down...')
|
logger.error('Unknown signal. Shutting down...')
|
||||||
plexpy.shutdown()
|
jellypy.shutdown()
|
||||||
|
|
||||||
plexpy.SIGNAL = None
|
jellypy.SIGNAL = None
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
24
PlexPy.py
24
PlexPy.py
@@ -1,24 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# This file is part of Tautulli.
|
|
||||||
#
|
|
||||||
# Tautulli is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# Tautulli is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
from Tautulli import main
|
|
||||||
|
|
||||||
# Call main() from Tautulli.py
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
# Tautulli
|
# JellyPy
|
||||||
|
|
||||||
A python based web application for monitoring, analytics and notifications for [Plex Media Server](https://plex.tv).
|
A python based web application for monitoring, analytics and notifications for [Jellyfin](https://jellyfin.org/).
|
||||||
|
|
||||||
This project is based on code from [Headphones](https://github.com/rembo10/headphones) and [PlexWatchWeb](https://github.com/ecleese/plexWatchWeb).
|
This project is based on [Tautulli](https://github.com/Tautulli/Tautulli).
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -64,3 +64,5 @@ This project is based on code from [Headphones](https://github.com/rembo10/headp
|
|||||||
This is free software under the GPL v3 open source license. Feel free to do with it what you wish, but any modification must be open sourced. A copy of the license is included.
|
This is free software under the GPL v3 open source license. Feel free to do with it what you wish, but any modification must be open sourced. A copy of the license is included.
|
||||||
|
|
||||||
This software includes Highsoft software libraries which you may freely distribute for non-commercial use. Commerical users must licence this software, for more information visit https://shop.highsoft.com/faq/non-commercial#non-commercial-redistribution.
|
This software includes Highsoft software libraries which you may freely distribute for non-commercial use. Commerical users must licence this software, for more information visit https://shop.highsoft.com/faq/non-commercial#non-commercial-redistribution.
|
||||||
|
|
||||||
|
[Tautulli]: https://github.com/Tautulli/Tautulli
|
||||||
@@ -2024,7 +2024,7 @@ Rating: {rating}/10 --> Rating: /10
|
|||||||
cache: false,
|
cache: false,
|
||||||
async: true,
|
async: true,
|
||||||
complete: function(xhr, status) {
|
complete: function(xhr, status) {
|
||||||
$("#plexpy-configuration-table").html(xhr.responseText);
|
$("#jellypy-configuration-table").html(xhr.responseText);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2035,7 +2035,7 @@ Rating: {rating}/10 --> Rating: /10
|
|||||||
cache: false,
|
cache: false,
|
||||||
async: true,
|
async: true,
|
||||||
complete: function(xhr, status) {
|
complete: function(xhr, status) {
|
||||||
$("#plexpy-scheduler-table").html(xhr.responseText);
|
$("#jellypy-scheduler-table").html(xhr.responseText);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2046,7 +2046,7 @@ Rating: {rating}/10 --> Rating: /10
|
|||||||
cache: false,
|
cache: false,
|
||||||
async: true,
|
async: true,
|
||||||
complete: function(xhr, status) {
|
complete: function(xhr, status) {
|
||||||
$("#plexpy-notifiers-table").html(xhr.responseText);
|
$("#jellypy-notifiers-table").html(xhr.responseText);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2071,7 +2071,7 @@ Rating: {rating}/10 --> Rating: /10
|
|||||||
cache: false,
|
cache: false,
|
||||||
async: true,
|
async: true,
|
||||||
complete: function(xhr, status) {
|
complete: function(xhr, status) {
|
||||||
$("#plexpy-newsletters-table").html(xhr.responseText);
|
$("#jellypy-newsletters-table").html(xhr.responseText);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2096,7 +2096,7 @@ Rating: {rating}/10 --> Rating: /10
|
|||||||
cache: false,
|
cache: false,
|
||||||
async: true,
|
async: true,
|
||||||
complete: function(xhr, status) {
|
complete: function(xhr, status) {
|
||||||
$("#plexpy-mobile-devices-table").html(xhr.responseText);
|
$("#jellypy-mobile-devices-table").html(xhr.responseText);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,73 +13,48 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
from future.builtins import range
|
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
import future.moves.queue as queue
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import sys
|
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
import future.moves.queue as queue
|
||||||
|
from future.builtins import range
|
||||||
|
|
||||||
# Some cut down versions of Python may not include this module and it's not critical for us
|
# Some cut down versions of Python may not include this module and it's not critical for us
|
||||||
try:
|
try:
|
||||||
import webbrowser
|
import webbrowser
|
||||||
|
|
||||||
no_browser = False
|
no_browser = False
|
||||||
except ImportError:
|
except ImportError:
|
||||||
no_browser = True
|
no_browser = True
|
||||||
|
|
||||||
from apscheduler.schedulers.background import BackgroundScheduler
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
from apscheduler.triggers.interval import IntervalTrigger
|
from apscheduler.triggers.interval import IntervalTrigger
|
||||||
from UniversalAnalytics import Tracker
|
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
PYTHON2 = sys.version_info[0] == 2
|
from jellypy import activity_handler
|
||||||
|
from jellypy import activity_pinger
|
||||||
if PYTHON2:
|
from jellypy import common
|
||||||
import activity_handler
|
from jellypy import database
|
||||||
import activity_pinger
|
from jellypy import datafactory
|
||||||
import common
|
from jellypy import exporter
|
||||||
import database
|
from jellypy import libraries
|
||||||
import datafactory
|
from jellypy import logger
|
||||||
import exporter
|
from jellypy import mobile_app
|
||||||
import libraries
|
from jellypy import newsletters
|
||||||
import logger
|
from jellypy import newsletter_handler
|
||||||
import mobile_app
|
from jellypy import notification_handler
|
||||||
import newsletters
|
from jellypy import notifiers
|
||||||
import newsletter_handler
|
from jellypy import plextv
|
||||||
import notification_handler
|
from jellypy import users
|
||||||
import notifiers
|
from jellypy import versioncheck
|
||||||
import plextv
|
from jellypy import web_socket
|
||||||
import users
|
from jellypy import webstart
|
||||||
import versioncheck
|
from jellypy import config
|
||||||
import web_socket
|
|
||||||
import webstart
|
|
||||||
import config
|
|
||||||
else:
|
|
||||||
from plexpy import activity_handler
|
|
||||||
from plexpy import activity_pinger
|
|
||||||
from plexpy import common
|
|
||||||
from plexpy import database
|
|
||||||
from plexpy import datafactory
|
|
||||||
from plexpy import exporter
|
|
||||||
from plexpy import libraries
|
|
||||||
from plexpy import logger
|
|
||||||
from plexpy import mobile_app
|
|
||||||
from plexpy import newsletters
|
|
||||||
from plexpy import newsletter_handler
|
|
||||||
from plexpy import notification_handler
|
|
||||||
from plexpy import notifiers
|
|
||||||
from plexpy import plextv
|
|
||||||
from plexpy import users
|
|
||||||
from plexpy import versioncheck
|
|
||||||
from plexpy import web_socket
|
|
||||||
from plexpy import webstart
|
|
||||||
from plexpy import config
|
|
||||||
|
|
||||||
|
|
||||||
PROG_DIR = None
|
PROG_DIR = None
|
||||||
FULL_PATH = None
|
FULL_PATH = None
|
||||||
@@ -502,7 +477,7 @@ def initialize_scheduler():
|
|||||||
|
|
||||||
def schedule_job(func, name, hours=0, minutes=0, seconds=0, args=None):
|
def schedule_job(func, name, hours=0, minutes=0, seconds=0, args=None):
|
||||||
"""
|
"""
|
||||||
Start scheduled job if starting or restarting plexpy.
|
Start scheduled job if starting or restarting jellypy.
|
||||||
Reschedule job if Interval Settings have changed.
|
Reschedule job if Interval Settings have changed.
|
||||||
Remove job if if Interval Settings changed to 0
|
Remove job if if Interval Settings changed to 0
|
||||||
|
|
||||||
@@ -2388,47 +2363,6 @@ def generate_uuid():
|
|||||||
return uuid.uuid4().hex
|
return uuid.uuid4().hex
|
||||||
|
|
||||||
|
|
||||||
def initialize_tracker():
|
|
||||||
data = {
|
|
||||||
'dataSource': 'server',
|
|
||||||
'appName': common.PRODUCT,
|
|
||||||
'appVersion': common.RELEASE,
|
|
||||||
'appId': INSTALL_TYPE,
|
|
||||||
'appInstallerId': CONFIG.GIT_BRANCH,
|
|
||||||
'dimension1': '{} {}'.format(common.PLATFORM, common.PLATFORM_RELEASE), # App Platform
|
|
||||||
'dimension2': common.PLATFORM_LINUX_DISTRO, # Linux Distro
|
|
||||||
'dimension3': common.PYTHON_VERSION,
|
|
||||||
'userLanguage': SYS_LANGUAGE,
|
|
||||||
'documentEncoding': SYS_ENCODING,
|
|
||||||
'noninteractive': True
|
|
||||||
}
|
|
||||||
|
|
||||||
tracker = Tracker.create('UA-111522699-2', client_id=CONFIG.PMS_UUID, hash_client_id=True,
|
|
||||||
user_agent=common.USER_AGENT)
|
|
||||||
tracker.set(data)
|
|
||||||
|
|
||||||
return tracker
|
|
||||||
|
|
||||||
|
|
||||||
def analytics_event(category, action, label=None, value=None, **kwargs):
|
|
||||||
data = {'category': category, 'action': action}
|
|
||||||
|
|
||||||
if label is not None:
|
|
||||||
data['label'] = label
|
|
||||||
|
|
||||||
if value is not None:
|
|
||||||
data['value'] = value
|
|
||||||
|
|
||||||
if kwargs:
|
|
||||||
data.update(kwargs)
|
|
||||||
|
|
||||||
if TRACKER:
|
|
||||||
try:
|
|
||||||
TRACKER.send('event', data)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warn("Failed to send analytics event for category '%s', action '%s': %s" % (category, action, e))
|
|
||||||
|
|
||||||
|
|
||||||
def check_folder_writable(folder, fallback, name):
|
def check_folder_writable(folder, fallback, name):
|
||||||
if not folder:
|
if not folder:
|
||||||
folder = fallback
|
folder = fallback
|
||||||
@@ -24,8 +24,8 @@ import time
|
|||||||
from apscheduler.triggers.date import DateTrigger
|
from apscheduler.triggers.date import DateTrigger
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
import activity_processor
|
import activity_processor
|
||||||
import datafactory
|
import datafactory
|
||||||
import helpers
|
import helpers
|
||||||
@@ -33,12 +33,12 @@ if plexpy.PYTHON2:
|
|||||||
import notification_handler
|
import notification_handler
|
||||||
import pmsconnect
|
import pmsconnect
|
||||||
else:
|
else:
|
||||||
from plexpy import activity_processor
|
from jellypy import activity_processor
|
||||||
from plexpy import datafactory
|
from jellypy import datafactory
|
||||||
from plexpy import helpers
|
from jellypy import helpers
|
||||||
from plexpy import logger
|
from jellypy import logger
|
||||||
from plexpy import notification_handler
|
from jellypy import notification_handler
|
||||||
from plexpy import pmsconnect
|
from jellypy import pmsconnect
|
||||||
|
|
||||||
|
|
||||||
ACTIVITY_SCHED = None
|
ACTIVITY_SCHED = None
|
||||||
@@ -134,7 +134,7 @@ class ActivityHandler(object):
|
|||||||
str(session['rating_key']), session['full_title'], '[Live TV]' if session['live'] else ''))
|
str(session['rating_key']), session['full_title'], '[Live TV]' if session['live'] else ''))
|
||||||
|
|
||||||
# Send notification after updating db
|
# Send notification after updating db
|
||||||
#plexpy.NOTIFY_QUEUE.put({'stream_data': session.copy(), 'notify_action': 'on_play'})
|
#jellypy.NOTIFY_QUEUE.put({'stream_data': session.copy(), 'notify_action': 'on_play'})
|
||||||
|
|
||||||
# Write the new session to our temp session table
|
# Write the new session to our temp session table
|
||||||
self.update_db_session(session=session, notify=True)
|
self.update_db_session(session=session, notify=True)
|
||||||
@@ -162,7 +162,7 @@ class ActivityHandler(object):
|
|||||||
# Retrieve the session data from our temp table
|
# Retrieve the session data from our temp table
|
||||||
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
||||||
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_stop'})
|
jellypy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_stop'})
|
||||||
|
|
||||||
# Write it to the history table
|
# Write it to the history table
|
||||||
monitor_proc = activity_processor.ActivityProcessor()
|
monitor_proc = activity_processor.ActivityProcessor()
|
||||||
@@ -198,7 +198,7 @@ class ActivityHandler(object):
|
|||||||
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
||||||
|
|
||||||
if not still_paused:
|
if not still_paused:
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_pause'})
|
jellypy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_pause'})
|
||||||
|
|
||||||
def on_resume(self):
|
def on_resume(self):
|
||||||
if self.is_valid_session():
|
if self.is_valid_session():
|
||||||
@@ -214,7 +214,7 @@ class ActivityHandler(object):
|
|||||||
# Retrieve the session data from our temp table
|
# Retrieve the session data from our temp table
|
||||||
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
||||||
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_resume'})
|
jellypy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_resume'})
|
||||||
|
|
||||||
def on_change(self):
|
def on_change(self):
|
||||||
if self.is_valid_session():
|
if self.is_valid_session():
|
||||||
@@ -227,7 +227,7 @@ class ActivityHandler(object):
|
|||||||
ap = activity_processor.ActivityProcessor()
|
ap = activity_processor.ActivityProcessor()
|
||||||
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
||||||
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_change'})
|
jellypy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_change'})
|
||||||
|
|
||||||
def on_buffer(self):
|
def on_buffer(self):
|
||||||
if self.is_valid_session():
|
if self.is_valid_session():
|
||||||
@@ -255,14 +255,14 @@ class ActivityHandler(object):
|
|||||||
(self.get_session_key(), buffer_last_triggered))
|
(self.get_session_key(), buffer_last_triggered))
|
||||||
time_since_last_trigger = helpers.timestamp() - int(buffer_last_triggered)
|
time_since_last_trigger = helpers.timestamp() - int(buffer_last_triggered)
|
||||||
|
|
||||||
if current_buffer_count >= plexpy.CONFIG.BUFFER_THRESHOLD and time_since_last_trigger == 0 or \
|
if current_buffer_count >= jellypy.CONFIG.BUFFER_THRESHOLD and time_since_last_trigger == 0 or \
|
||||||
time_since_last_trigger >= plexpy.CONFIG.BUFFER_WAIT:
|
time_since_last_trigger >= jellypy.CONFIG.BUFFER_WAIT:
|
||||||
ap.set_session_buffer_trigger_time(session_key=self.get_session_key())
|
ap.set_session_buffer_trigger_time(session_key=self.get_session_key())
|
||||||
|
|
||||||
# Retrieve the session data from our temp table
|
# Retrieve the session data from our temp table
|
||||||
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
||||||
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_buffer'})
|
jellypy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_buffer'})
|
||||||
|
|
||||||
def on_error(self):
|
def on_error(self):
|
||||||
if self.is_valid_session():
|
if self.is_valid_session():
|
||||||
@@ -275,7 +275,7 @@ class ActivityHandler(object):
|
|||||||
ap = activity_processor.ActivityProcessor()
|
ap = activity_processor.ActivityProcessor()
|
||||||
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
||||||
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_error'})
|
jellypy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(), 'notify_action': 'on_error'})
|
||||||
|
|
||||||
# This function receives events from our websocket connection
|
# This function receives events from our websocket connection
|
||||||
def process(self):
|
def process(self):
|
||||||
@@ -358,10 +358,10 @@ class ActivityHandler(object):
|
|||||||
# The only purpose of this is for notifications
|
# The only purpose of this is for notifications
|
||||||
if not db_session['watched'] and this_state != 'buffering':
|
if not db_session['watched'] and this_state != 'buffering':
|
||||||
progress_percent = helpers.get_percent(self.timeline['viewOffset'], db_session['duration'])
|
progress_percent = helpers.get_percent(self.timeline['viewOffset'], db_session['duration'])
|
||||||
watched_percent = {'movie': plexpy.CONFIG.MOVIE_WATCHED_PERCENT,
|
watched_percent = {'movie': jellypy.CONFIG.MOVIE_WATCHED_PERCENT,
|
||||||
'episode': plexpy.CONFIG.TV_WATCHED_PERCENT,
|
'episode': jellypy.CONFIG.TV_WATCHED_PERCENT,
|
||||||
'track': plexpy.CONFIG.MUSIC_WATCHED_PERCENT,
|
'track': jellypy.CONFIG.MUSIC_WATCHED_PERCENT,
|
||||||
'clip': plexpy.CONFIG.TV_WATCHED_PERCENT
|
'clip': jellypy.CONFIG.TV_WATCHED_PERCENT
|
||||||
}
|
}
|
||||||
|
|
||||||
if progress_percent >= watched_percent.get(db_session['media_type'], 101):
|
if progress_percent >= watched_percent.get(db_session['media_type'], 101):
|
||||||
@@ -373,7 +373,7 @@ class ActivityHandler(object):
|
|||||||
session=db_session, notify_action='on_watched', notified=False)
|
session=db_session, notify_action='on_watched', notified=False)
|
||||||
|
|
||||||
for d in watched_notifiers:
|
for d in watched_notifiers:
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(),
|
jellypy.NOTIFY_QUEUE.put({'stream_data': db_session.copy(),
|
||||||
'notifier_id': d['notifier_id'],
|
'notifier_id': d['notifier_id'],
|
||||||
'notify_action': 'on_watched'})
|
'notify_action': 'on_watched'})
|
||||||
|
|
||||||
@@ -440,7 +440,7 @@ class TimelineHandler(object):
|
|||||||
# Add a new media item to the recently added queue
|
# Add a new media item to the recently added queue
|
||||||
if media_type and section_id > 0 and \
|
if media_type and section_id > 0 and \
|
||||||
((state_type == 0 and metadata_state == 'created')): # or \
|
((state_type == 0 and metadata_state == 'created')): # or \
|
||||||
#(plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_UPGRADE and state_type in (1, 5) and \
|
#(jellypy.CONFIG.NOTIFY_RECENTLY_ADDED_UPGRADE and state_type in (1, 5) and \
|
||||||
#media_state == 'analyzing' and queue_size is None)):
|
#media_state == 'analyzing' and queue_size is None)):
|
||||||
|
|
||||||
if media_type in ('episode', 'track'):
|
if media_type in ('episode', 'track'):
|
||||||
@@ -467,7 +467,7 @@ class TimelineHandler(object):
|
|||||||
schedule_callback('rating_key-{}'.format(grandparent_rating_key),
|
schedule_callback('rating_key-{}'.format(grandparent_rating_key),
|
||||||
func=clear_recently_added_queue,
|
func=clear_recently_added_queue,
|
||||||
args=[grandparent_rating_key, grandparent_title],
|
args=[grandparent_rating_key, grandparent_title],
|
||||||
seconds=plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_DELAY)
|
seconds=jellypy.CONFIG.NOTIFY_RECENTLY_ADDED_DELAY)
|
||||||
|
|
||||||
elif media_type in ('season', 'album'):
|
elif media_type in ('season', 'album'):
|
||||||
metadata = self.get_metadata()
|
metadata = self.get_metadata()
|
||||||
@@ -486,7 +486,7 @@ class TimelineHandler(object):
|
|||||||
schedule_callback('rating_key-{}'.format(parent_rating_key),
|
schedule_callback('rating_key-{}'.format(parent_rating_key),
|
||||||
func=clear_recently_added_queue,
|
func=clear_recently_added_queue,
|
||||||
args=[parent_rating_key, parent_title],
|
args=[parent_rating_key, parent_title],
|
||||||
seconds=plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_DELAY)
|
seconds=jellypy.CONFIG.NOTIFY_RECENTLY_ADDED_DELAY)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
queue_set = RECENTLY_ADDED_QUEUE.get(rating_key, set())
|
queue_set = RECENTLY_ADDED_QUEUE.get(rating_key, set())
|
||||||
@@ -499,7 +499,7 @@ class TimelineHandler(object):
|
|||||||
schedule_callback('rating_key-{}'.format(rating_key),
|
schedule_callback('rating_key-{}'.format(rating_key),
|
||||||
func=clear_recently_added_queue,
|
func=clear_recently_added_queue,
|
||||||
args=[rating_key, title],
|
args=[rating_key, title],
|
||||||
seconds=plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_DELAY)
|
seconds=jellypy.CONFIG.NOTIFY_RECENTLY_ADDED_DELAY)
|
||||||
|
|
||||||
# A movie, show, or artist is done processing
|
# A movie, show, or artist is done processing
|
||||||
elif media_type in ('movie', 'show', 'artist') and section_id > 0 and \
|
elif media_type in ('movie', 'show', 'artist') and section_id > 0 and \
|
||||||
@@ -536,10 +536,10 @@ class ReachabilityHandler(object):
|
|||||||
return helpers.bool_true(pref)
|
return helpers.bool_true(pref)
|
||||||
|
|
||||||
def on_down(self, server_response):
|
def on_down(self, server_response):
|
||||||
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_extdown', 'remote_access_info': server_response})
|
jellypy.NOTIFY_QUEUE.put({'notify_action': 'on_extdown', 'remote_access_info': server_response})
|
||||||
|
|
||||||
def on_up(self, server_response):
|
def on_up(self, server_response):
|
||||||
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_extup', 'remote_access_info': server_response})
|
jellypy.NOTIFY_QUEUE.put({'notify_action': 'on_extup', 'remote_access_info': server_response})
|
||||||
|
|
||||||
def process(self):
|
def process(self):
|
||||||
# Check if remote access is enabled
|
# Check if remote access is enabled
|
||||||
@@ -547,7 +547,7 @@ class ReachabilityHandler(object):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Do nothing if remote access is still up and hasn't changed
|
# Do nothing if remote access is still up and hasn't changed
|
||||||
if self.is_reachable() and plexpy.PLEX_REMOTE_ACCESS_UP:
|
if self.is_reachable() and jellypy.PLEX_REMOTE_ACCESS_UP:
|
||||||
return
|
return
|
||||||
|
|
||||||
pms_connect = pmsconnect.PmsConnect()
|
pms_connect = pmsconnect.PmsConnect()
|
||||||
@@ -558,22 +558,22 @@ class ReachabilityHandler(object):
|
|||||||
if server_response['mapping_state'] == 'waiting':
|
if server_response['mapping_state'] == 'waiting':
|
||||||
logger.warn("Tautulli ReachabilityHandler :: Remote access waiting for port mapping.")
|
logger.warn("Tautulli ReachabilityHandler :: Remote access waiting for port mapping.")
|
||||||
|
|
||||||
elif plexpy.PLEX_REMOTE_ACCESS_UP is not False and server_response['reason']:
|
elif jellypy.PLEX_REMOTE_ACCESS_UP is not False and server_response['reason']:
|
||||||
logger.warn("Tautulli ReachabilityHandler :: Remote access failed: %s" % server_response['reason'])
|
logger.warn("Tautulli ReachabilityHandler :: Remote access failed: %s" % server_response['reason'])
|
||||||
logger.info("Tautulli ReachabilityHandler :: Plex remote access is down.")
|
logger.info("Tautulli ReachabilityHandler :: Plex remote access is down.")
|
||||||
|
|
||||||
plexpy.PLEX_REMOTE_ACCESS_UP = False
|
jellypy.PLEX_REMOTE_ACCESS_UP = False
|
||||||
|
|
||||||
if not ACTIVITY_SCHED.get_job('on_extdown'):
|
if not ACTIVITY_SCHED.get_job('on_extdown'):
|
||||||
logger.debug("Tautulli ReachabilityHandler :: Schedule remote access down callback in %d seconds.",
|
logger.debug("Tautulli ReachabilityHandler :: Schedule remote access down callback in %d seconds.",
|
||||||
plexpy.CONFIG.NOTIFY_REMOTE_ACCESS_THRESHOLD)
|
jellypy.CONFIG.NOTIFY_REMOTE_ACCESS_THRESHOLD)
|
||||||
schedule_callback('on_extdown', func=self.on_down, args=[server_response],
|
schedule_callback('on_extdown', func=self.on_down, args=[server_response],
|
||||||
seconds=plexpy.CONFIG.NOTIFY_REMOTE_ACCESS_THRESHOLD)
|
seconds=jellypy.CONFIG.NOTIFY_REMOTE_ACCESS_THRESHOLD)
|
||||||
|
|
||||||
elif plexpy.PLEX_REMOTE_ACCESS_UP is False and not server_response['reason']:
|
elif jellypy.PLEX_REMOTE_ACCESS_UP is False and not server_response['reason']:
|
||||||
logger.info("Tautulli ReachabilityHandler :: Plex remote access is back up.")
|
logger.info("Tautulli ReachabilityHandler :: Plex remote access is back up.")
|
||||||
|
|
||||||
plexpy.PLEX_REMOTE_ACCESS_UP = True
|
jellypy.PLEX_REMOTE_ACCESS_UP = True
|
||||||
|
|
||||||
if ACTIVITY_SCHED.get_job('on_extdown'):
|
if ACTIVITY_SCHED.get_job('on_extdown'):
|
||||||
logger.debug("Tautulli ReachabilityHandler :: Cancelling scheduled remote access down callback.")
|
logger.debug("Tautulli ReachabilityHandler :: Cancelling scheduled remote access down callback.")
|
||||||
@@ -581,8 +581,8 @@ class ReachabilityHandler(object):
|
|||||||
else:
|
else:
|
||||||
self.on_up(server_response)
|
self.on_up(server_response)
|
||||||
|
|
||||||
elif plexpy.PLEX_REMOTE_ACCESS_UP is None:
|
elif jellypy.PLEX_REMOTE_ACCESS_UP is None:
|
||||||
plexpy.PLEX_REMOTE_ACCESS_UP = self.is_reachable()
|
jellypy.PLEX_REMOTE_ACCESS_UP = self.is_reachable()
|
||||||
|
|
||||||
|
|
||||||
def del_keys(key):
|
def del_keys(key):
|
||||||
@@ -626,7 +626,7 @@ def force_stop_stream(session_key, title, user):
|
|||||||
else:
|
else:
|
||||||
session['write_attempts'] += 1
|
session['write_attempts'] += 1
|
||||||
|
|
||||||
if session['write_attempts'] < plexpy.CONFIG.SESSION_DB_WRITE_ATTEMPTS:
|
if session['write_attempts'] < jellypy.CONFIG.SESSION_DB_WRITE_ATTEMPTS:
|
||||||
logger.warn("Tautulli ActivityHandler :: Failed to write stream with sessionKey %s ratingKey %s to the database. " \
|
logger.warn("Tautulli ActivityHandler :: Failed to write stream with sessionKey %s ratingKey %s to the database. " \
|
||||||
"Will try again in 30 seconds. Write attempt %s."
|
"Will try again in 30 seconds. Write attempt %s."
|
||||||
% (session['session_key'], session['rating_key'], str(session['write_attempts'])))
|
% (session['session_key'], session['rating_key'], str(session['write_attempts'])))
|
||||||
@@ -649,14 +649,14 @@ def force_stop_stream(session_key, title, user):
|
|||||||
def clear_recently_added_queue(rating_key, title):
|
def clear_recently_added_queue(rating_key, title):
|
||||||
child_keys = RECENTLY_ADDED_QUEUE[rating_key]
|
child_keys = RECENTLY_ADDED_QUEUE[rating_key]
|
||||||
|
|
||||||
if plexpy.CONFIG.NOTIFY_GROUP_RECENTLY_ADDED_GRANDPARENT and len(child_keys) > 1:
|
if jellypy.CONFIG.NOTIFY_GROUP_RECENTLY_ADDED_GRANDPARENT and len(child_keys) > 1:
|
||||||
on_created(rating_key, child_keys=child_keys)
|
on_created(rating_key, child_keys=child_keys)
|
||||||
|
|
||||||
elif child_keys:
|
elif child_keys:
|
||||||
for child_key in child_keys:
|
for child_key in child_keys:
|
||||||
grandchild_keys = RECENTLY_ADDED_QUEUE.get(child_key, [])
|
grandchild_keys = RECENTLY_ADDED_QUEUE.get(child_key, [])
|
||||||
|
|
||||||
if plexpy.CONFIG.NOTIFY_GROUP_RECENTLY_ADDED_PARENT and len(grandchild_keys) > 1:
|
if jellypy.CONFIG.NOTIFY_GROUP_RECENTLY_ADDED_PARENT and len(grandchild_keys) > 1:
|
||||||
on_created(child_key, child_keys=grandchild_keys)
|
on_created(child_key, child_keys=grandchild_keys)
|
||||||
|
|
||||||
elif grandchild_keys:
|
elif grandchild_keys:
|
||||||
@@ -697,7 +697,7 @@ def on_created(rating_key, **kwargs):
|
|||||||
if notify:
|
if notify:
|
||||||
data = {'timeline_data': metadata, 'notify_action': 'on_created'}
|
data = {'timeline_data': metadata, 'notify_action': 'on_created'}
|
||||||
data.update(kwargs)
|
data.update(kwargs)
|
||||||
plexpy.NOTIFY_QUEUE.put(data)
|
jellypy.NOTIFY_QUEUE.put(data)
|
||||||
|
|
||||||
all_keys = [rating_key]
|
all_keys = [rating_key]
|
||||||
if 'child_keys' in kwargs:
|
if 'child_keys' in kwargs:
|
||||||
@@ -714,7 +714,7 @@ def on_created(rating_key, **kwargs):
|
|||||||
|
|
||||||
def delete_metadata_cache(session_key):
|
def delete_metadata_cache(session_key):
|
||||||
try:
|
try:
|
||||||
os.remove(os.path.join(plexpy.CONFIG.CACHE_DIR, 'session_metadata/metadata-sessionKey-%s.json' % session_key))
|
os.remove(os.path.join(jellypy.CONFIG.CACHE_DIR, 'session_metadata/metadata-sessionKey-%s.json' % session_key))
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
logger.error("Tautulli ActivityHandler :: Failed to remove metadata cache file (sessionKey %s): %s"
|
logger.error("Tautulli ActivityHandler :: Failed to remove metadata cache file (sessionKey %s): %s"
|
||||||
% (session_key, e))
|
% (session_key, e))
|
||||||
@@ -18,8 +18,8 @@ from future.builtins import str
|
|||||||
|
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
import activity_handler
|
import activity_handler
|
||||||
import activity_processor
|
import activity_processor
|
||||||
import database
|
import database
|
||||||
@@ -31,16 +31,16 @@ if plexpy.PYTHON2:
|
|||||||
import pmsconnect
|
import pmsconnect
|
||||||
import web_socket
|
import web_socket
|
||||||
else:
|
else:
|
||||||
from plexpy import activity_handler
|
from jellypy import activity_handler
|
||||||
from plexpy import activity_processor
|
from jellypy import activity_processor
|
||||||
from plexpy import database
|
from jellypy import database
|
||||||
from plexpy import helpers
|
from jellypy import helpers
|
||||||
from plexpy import libraries
|
from jellypy import libraries
|
||||||
from plexpy import logger
|
from jellypy import logger
|
||||||
from plexpy import notification_handler
|
from jellypy import notification_handler
|
||||||
from plexpy import plextv
|
from jellypy import plextv
|
||||||
from plexpy import pmsconnect
|
from jellypy import pmsconnect
|
||||||
from plexpy import web_socket
|
from jellypy import web_socket
|
||||||
|
|
||||||
|
|
||||||
monitor_lock = threading.Lock()
|
monitor_lock = threading.Lock()
|
||||||
@@ -82,28 +82,28 @@ def check_active_sessions(ws_request=False):
|
|||||||
if session['state'] == 'paused':
|
if session['state'] == 'paused':
|
||||||
logger.debug("Tautulli Monitor :: Session %s paused." % stream['session_key'])
|
logger.debug("Tautulli Monitor :: Session %s paused." % stream['session_key'])
|
||||||
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_pause'})
|
jellypy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_pause'})
|
||||||
|
|
||||||
if session['state'] == 'playing' and stream['state'] == 'paused':
|
if session['state'] == 'playing' and stream['state'] == 'paused':
|
||||||
logger.debug("Tautulli Monitor :: Session %s resumed." % stream['session_key'])
|
logger.debug("Tautulli Monitor :: Session %s resumed." % stream['session_key'])
|
||||||
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_resume'})
|
jellypy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_resume'})
|
||||||
|
|
||||||
if session['state'] == 'error':
|
if session['state'] == 'error':
|
||||||
logger.debug("Tautulli Monitor :: Session %s encountered an error." % stream['session_key'])
|
logger.debug("Tautulli Monitor :: Session %s encountered an error." % stream['session_key'])
|
||||||
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_error'})
|
jellypy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_error'})
|
||||||
|
|
||||||
if stream['state'] == 'paused' and not ws_request:
|
if stream['state'] == 'paused' and not ws_request:
|
||||||
# The stream is still paused so we need to increment the paused_counter
|
# The stream is still paused so we need to increment the paused_counter
|
||||||
# Using the set config parameter as the interval, probably not the most accurate but
|
# Using the set config parameter as the interval, probably not the most accurate but
|
||||||
# it will have to do for now. If it's a websocket request don't use this method.
|
# it will have to do for now. If it's a websocket request don't use this method.
|
||||||
paused_counter = int(stream['paused_counter']) + plexpy.CONFIG.MONITORING_INTERVAL
|
paused_counter = int(stream['paused_counter']) + jellypy.CONFIG.MONITORING_INTERVAL
|
||||||
monitor_db.action('UPDATE sessions SET paused_counter = ? '
|
monitor_db.action('UPDATE sessions SET paused_counter = ? '
|
||||||
'WHERE session_key = ? AND rating_key = ?',
|
'WHERE session_key = ? AND rating_key = ?',
|
||||||
[paused_counter, stream['session_key'], stream['rating_key']])
|
[paused_counter, stream['session_key'], stream['rating_key']])
|
||||||
|
|
||||||
if session['state'] == 'buffering' and plexpy.CONFIG.BUFFER_THRESHOLD > 0:
|
if session['state'] == 'buffering' and jellypy.CONFIG.BUFFER_THRESHOLD > 0:
|
||||||
# The stream is buffering so we need to increment the buffer_count
|
# The stream is buffering so we need to increment the buffer_count
|
||||||
# We're going just increment on every monitor ping,
|
# We're going just increment on every monitor ping,
|
||||||
# would be difficult to keep track otherwise
|
# would be difficult to keep track otherwise
|
||||||
@@ -117,11 +117,11 @@ def check_active_sessions(ws_request=False):
|
|||||||
'WHERE session_key = ? AND rating_key = ?',
|
'WHERE session_key = ? AND rating_key = ?',
|
||||||
[stream['session_key'], stream['rating_key']])
|
[stream['session_key'], stream['rating_key']])
|
||||||
|
|
||||||
if buffer_values[0]['buffer_count'] >= plexpy.CONFIG.BUFFER_THRESHOLD:
|
if buffer_values[0]['buffer_count'] >= jellypy.CONFIG.BUFFER_THRESHOLD:
|
||||||
# Push any notifications -
|
# Push any notifications -
|
||||||
# Push it on it's own thread so we don't hold up our db actions
|
# Push it on it's own thread so we don't hold up our db actions
|
||||||
# Our first buffer notification
|
# Our first buffer notification
|
||||||
if buffer_values[0]['buffer_count'] == plexpy.CONFIG.BUFFER_THRESHOLD:
|
if buffer_values[0]['buffer_count'] == jellypy.CONFIG.BUFFER_THRESHOLD:
|
||||||
logger.info("Tautulli Monitor :: User '%s' has triggered a buffer warning."
|
logger.info("Tautulli Monitor :: User '%s' has triggered a buffer warning."
|
||||||
% stream['user'])
|
% stream['user'])
|
||||||
# Set the buffer trigger time
|
# Set the buffer trigger time
|
||||||
@@ -130,12 +130,12 @@ def check_active_sessions(ws_request=False):
|
|||||||
'WHERE session_key = ? AND rating_key = ?',
|
'WHERE session_key = ? AND rating_key = ?',
|
||||||
[stream['session_key'], stream['rating_key']])
|
[stream['session_key'], stream['rating_key']])
|
||||||
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_buffer'})
|
jellypy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_buffer'})
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Subsequent buffer notifications after wait time
|
# Subsequent buffer notifications after wait time
|
||||||
if helpers.timestamp() > buffer_values[0]['buffer_last_triggered'] + \
|
if helpers.timestamp() > buffer_values[0]['buffer_last_triggered'] + \
|
||||||
plexpy.CONFIG.BUFFER_WAIT:
|
jellypy.CONFIG.BUFFER_WAIT:
|
||||||
logger.info("Tautulli Monitor :: User '%s' has triggered multiple buffer warnings."
|
logger.info("Tautulli Monitor :: User '%s' has triggered multiple buffer warnings."
|
||||||
% stream['user'])
|
% stream['user'])
|
||||||
# Set the buffer trigger time
|
# Set the buffer trigger time
|
||||||
@@ -144,7 +144,7 @@ def check_active_sessions(ws_request=False):
|
|||||||
'WHERE session_key = ? AND rating_key = ?',
|
'WHERE session_key = ? AND rating_key = ?',
|
||||||
[stream['session_key'], stream['rating_key']])
|
[stream['session_key'], stream['rating_key']])
|
||||||
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_buffer'})
|
jellypy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_buffer'})
|
||||||
|
|
||||||
logger.debug("Tautulli Monitor :: Session %s is buffering. Count is now %s. Last triggered %s."
|
logger.debug("Tautulli Monitor :: Session %s is buffering. Count is now %s. Last triggered %s."
|
||||||
% (stream['session_key'],
|
% (stream['session_key'],
|
||||||
@@ -157,11 +157,11 @@ def check_active_sessions(ws_request=False):
|
|||||||
if session['state'] != 'buffering':
|
if session['state'] != 'buffering':
|
||||||
progress_percent = helpers.get_percent(session['view_offset'], session['duration'])
|
progress_percent = helpers.get_percent(session['view_offset'], session['duration'])
|
||||||
notify_states = notification_handler.get_notify_state(session=session)
|
notify_states = notification_handler.get_notify_state(session=session)
|
||||||
if (session['media_type'] == 'movie' and progress_percent >= plexpy.CONFIG.MOVIE_WATCHED_PERCENT or
|
if (session['media_type'] == 'movie' and progress_percent >= jellypy.CONFIG.MOVIE_WATCHED_PERCENT or
|
||||||
session['media_type'] == 'episode' and progress_percent >= plexpy.CONFIG.TV_WATCHED_PERCENT or
|
session['media_type'] == 'episode' and progress_percent >= jellypy.CONFIG.TV_WATCHED_PERCENT or
|
||||||
session['media_type'] == 'track' and progress_percent >= plexpy.CONFIG.MUSIC_WATCHED_PERCENT) \
|
session['media_type'] == 'track' and progress_percent >= jellypy.CONFIG.MUSIC_WATCHED_PERCENT) \
|
||||||
and not any(d['notify_action'] == 'on_watched' for d in notify_states):
|
and not any(d['notify_action'] == 'on_watched' for d in notify_states):
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_watched'})
|
jellypy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_watched'})
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# The user has stopped playing a stream
|
# The user has stopped playing a stream
|
||||||
@@ -177,13 +177,13 @@ def check_active_sessions(ws_request=False):
|
|||||||
|
|
||||||
progress_percent = helpers.get_percent(stream['view_offset'], stream['duration'])
|
progress_percent = helpers.get_percent(stream['view_offset'], stream['duration'])
|
||||||
notify_states = notification_handler.get_notify_state(session=stream)
|
notify_states = notification_handler.get_notify_state(session=stream)
|
||||||
if (stream['media_type'] == 'movie' and progress_percent >= plexpy.CONFIG.MOVIE_WATCHED_PERCENT or
|
if (stream['media_type'] == 'movie' and progress_percent >= jellypy.CONFIG.MOVIE_WATCHED_PERCENT or
|
||||||
stream['media_type'] == 'episode' and progress_percent >= plexpy.CONFIG.TV_WATCHED_PERCENT or
|
stream['media_type'] == 'episode' and progress_percent >= jellypy.CONFIG.TV_WATCHED_PERCENT or
|
||||||
stream['media_type'] == 'track' and progress_percent >= plexpy.CONFIG.MUSIC_WATCHED_PERCENT) \
|
stream['media_type'] == 'track' and progress_percent >= jellypy.CONFIG.MUSIC_WATCHED_PERCENT) \
|
||||||
and not any(d['notify_action'] == 'on_watched' for d in notify_states):
|
and not any(d['notify_action'] == 'on_watched' for d in notify_states):
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_watched'})
|
jellypy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_watched'})
|
||||||
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_stop'})
|
jellypy.NOTIFY_QUEUE.put({'stream_data': stream.copy(), 'notify_action': 'on_stop'})
|
||||||
|
|
||||||
# Write the item history on playback stop
|
# Write the item history on playback stop
|
||||||
row_id = monitor_process.write_session_history(session=stream)
|
row_id = monitor_process.write_session_history(session=stream)
|
||||||
@@ -196,7 +196,7 @@ def check_active_sessions(ws_request=False):
|
|||||||
else:
|
else:
|
||||||
stream['write_attempts'] += 1
|
stream['write_attempts'] += 1
|
||||||
|
|
||||||
if stream['write_attempts'] < plexpy.CONFIG.SESSION_DB_WRITE_ATTEMPTS:
|
if stream['write_attempts'] < jellypy.CONFIG.SESSION_DB_WRITE_ATTEMPTS:
|
||||||
logger.warn("Tautulli Monitor :: Failed to write sessionKey %s ratingKey %s to the database. " \
|
logger.warn("Tautulli Monitor :: Failed to write sessionKey %s ratingKey %s to the database. " \
|
||||||
"Will try again on the next pass. Write attempt %s."
|
"Will try again on the next pass. Write attempt %s."
|
||||||
% (stream['session_key'], stream['rating_key'], str(stream['write_attempts'])))
|
% (stream['session_key'], stream['rating_key'], str(stream['write_attempts'])))
|
||||||
@@ -223,7 +223,7 @@ def check_active_sessions(ws_request=False):
|
|||||||
|
|
||||||
|
|
||||||
def connect_server(log=True, startup=False):
|
def connect_server(log=True, startup=False):
|
||||||
if plexpy.CONFIG.PMS_IS_CLOUD:
|
if jellypy.CONFIG.PMS_IS_CLOUD:
|
||||||
if log:
|
if log:
|
||||||
logger.info("Tautulli Monitor :: Checking for Plex Cloud server status...")
|
logger.info("Tautulli Monitor :: Checking for Plex Cloud server status...")
|
||||||
|
|
||||||
@@ -264,12 +264,12 @@ def check_server_updates():
|
|||||||
download_info = plex_tv.get_plex_downloads()
|
download_info = plex_tv.get_plex_downloads()
|
||||||
|
|
||||||
if download_info:
|
if download_info:
|
||||||
logger.info("Tautulli Monitor :: Current PMS version: %s", plexpy.CONFIG.PMS_VERSION)
|
logger.info("Tautulli Monitor :: Current PMS version: %s", jellypy.CONFIG.PMS_VERSION)
|
||||||
|
|
||||||
if download_info['update_available']:
|
if download_info['update_available']:
|
||||||
logger.info("Tautulli Monitor :: PMS update available version: %s", download_info['version'])
|
logger.info("Tautulli Monitor :: PMS update available version: %s", download_info['version'])
|
||||||
|
|
||||||
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_pmsupdate', 'pms_download_info': download_info})
|
jellypy.NOTIFY_QUEUE.put({'notify_action': 'on_pmsupdate', 'pms_download_info': download_info})
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logger.info("Tautulli Monitor :: No PMS update available.")
|
logger.info("Tautulli Monitor :: No PMS update available.")
|
||||||
@@ -20,8 +20,8 @@ from future.builtins import object
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
import database
|
import database
|
||||||
import helpers
|
import helpers
|
||||||
import libraries
|
import libraries
|
||||||
@@ -29,12 +29,12 @@ if plexpy.PYTHON2:
|
|||||||
import pmsconnect
|
import pmsconnect
|
||||||
import users
|
import users
|
||||||
else:
|
else:
|
||||||
from plexpy import database
|
from jellypy import database
|
||||||
from plexpy import helpers
|
from jellypy import helpers
|
||||||
from plexpy import libraries
|
from jellypy import libraries
|
||||||
from plexpy import logger
|
from jellypy import logger
|
||||||
from plexpy import pmsconnect
|
from jellypy import pmsconnect
|
||||||
from plexpy import users
|
from jellypy import users
|
||||||
|
|
||||||
|
|
||||||
class ActivityProcessor(object):
|
class ActivityProcessor(object):
|
||||||
@@ -165,7 +165,7 @@ class ActivityProcessor(object):
|
|||||||
# Check if any notification agents have notifications enabled
|
# Check if any notification agents have notifications enabled
|
||||||
if notify:
|
if notify:
|
||||||
session.update(timestamp)
|
session.update(timestamp)
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': session.copy(), 'notify_action': 'on_play'})
|
jellypy.NOTIFY_QUEUE.put({'stream_data': session.copy(), 'notify_action': 'on_play'})
|
||||||
|
|
||||||
# Add Live TV library if it hasn't been added
|
# Add Live TV library if it hasn't been added
|
||||||
if values['live']:
|
if values['live']:
|
||||||
@@ -231,14 +231,14 @@ class ActivityProcessor(object):
|
|||||||
|
|
||||||
real_play_time = stopped - helpers.cast_to_int(session['started']) - helpers.cast_to_int(session['paused_counter'])
|
real_play_time = stopped - helpers.cast_to_int(session['started']) - helpers.cast_to_int(session['paused_counter'])
|
||||||
|
|
||||||
if not is_import and plexpy.CONFIG.LOGGING_IGNORE_INTERVAL:
|
if not is_import and jellypy.CONFIG.LOGGING_IGNORE_INTERVAL:
|
||||||
if (session['media_type'] == 'movie' or session['media_type'] == 'episode') and \
|
if (session['media_type'] == 'movie' or session['media_type'] == 'episode') and \
|
||||||
(real_play_time < int(plexpy.CONFIG.LOGGING_IGNORE_INTERVAL)):
|
(real_play_time < int(jellypy.CONFIG.LOGGING_IGNORE_INTERVAL)):
|
||||||
logging_enabled = False
|
logging_enabled = False
|
||||||
logger.debug("Tautulli ActivityProcessor :: Play duration for session %s ratingKey %s is %s secs "
|
logger.debug("Tautulli ActivityProcessor :: Play duration for session %s ratingKey %s is %s secs "
|
||||||
"which is less than %s seconds, so we're not logging it." %
|
"which is less than %s seconds, so we're not logging it." %
|
||||||
(session['session_key'], session['rating_key'], str(real_play_time),
|
(session['session_key'], session['rating_key'], str(real_play_time),
|
||||||
plexpy.CONFIG.LOGGING_IGNORE_INTERVAL))
|
jellypy.CONFIG.LOGGING_IGNORE_INTERVAL))
|
||||||
if not is_import and session['media_type'] == 'track':
|
if not is_import and session['media_type'] == 'track':
|
||||||
if real_play_time < 15 and helpers.cast_to_int(session['duration']) >= 30:
|
if real_play_time < 15 and helpers.cast_to_int(session['duration']) >= 30:
|
||||||
logging_enabled = False
|
logging_enabled = False
|
||||||
@@ -360,9 +360,9 @@ class ActivityProcessor(object):
|
|||||||
'view_offset': result[1]['view_offset'],
|
'view_offset': result[1]['view_offset'],
|
||||||
'reference_id': result[1]['reference_id']}
|
'reference_id': result[1]['reference_id']}
|
||||||
|
|
||||||
watched_percent = {'movie': plexpy.CONFIG.MOVIE_WATCHED_PERCENT,
|
watched_percent = {'movie': jellypy.CONFIG.MOVIE_WATCHED_PERCENT,
|
||||||
'episode': plexpy.CONFIG.TV_WATCHED_PERCENT,
|
'episode': jellypy.CONFIG.TV_WATCHED_PERCENT,
|
||||||
'track': plexpy.CONFIG.MUSIC_WATCHED_PERCENT
|
'track': jellypy.CONFIG.MUSIC_WATCHED_PERCENT
|
||||||
}
|
}
|
||||||
prev_progress_percent = helpers.get_percent(prev_session['view_offset'], session['duration'])
|
prev_progress_percent = helpers.get_percent(prev_session['view_offset'], session['duration'])
|
||||||
media_watched_percent = watched_percent.get(session['media_type'], 0)
|
media_watched_percent = watched_percent.get(session['media_type'], 0)
|
||||||
@@ -661,4 +661,4 @@ class ActivityProcessor(object):
|
|||||||
'WHERE user_id = ? AND machine_id = ? AND media_type = ? '
|
'WHERE user_id = ? AND machine_id = ? AND media_type = ? '
|
||||||
'ORDER BY stopped DESC',
|
'ORDER BY stopped DESC',
|
||||||
[user_id, machine_id, media_type])
|
[user_id, machine_id, media_type])
|
||||||
return int(started - last_session.get('stopped', 0) >= plexpy.CONFIG.NOTIFY_CONTINUED_SESSION_THRESHOLD)
|
return int(started - last_session.get('stopped', 0) >= jellypy.CONFIG.NOTIFY_CONTINUED_SESSION_THRESHOLD)
|
||||||
@@ -35,8 +35,8 @@ import traceback
|
|||||||
import cherrypy
|
import cherrypy
|
||||||
import xmltodict
|
import xmltodict
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
import common
|
import common
|
||||||
import config
|
import config
|
||||||
import database
|
import database
|
||||||
@@ -51,19 +51,19 @@ if plexpy.PYTHON2:
|
|||||||
import plextv
|
import plextv
|
||||||
import users
|
import users
|
||||||
else:
|
else:
|
||||||
from plexpy import common
|
from jellypy import common
|
||||||
from plexpy import config
|
from jellypy import config
|
||||||
from plexpy import database
|
from jellypy import database
|
||||||
from plexpy import helpers
|
from jellypy import helpers
|
||||||
from plexpy import libraries
|
from jellypy import libraries
|
||||||
from plexpy import logger
|
from jellypy import logger
|
||||||
from plexpy import mobile_app
|
from jellypy import mobile_app
|
||||||
from plexpy import notification_handler
|
from jellypy import notification_handler
|
||||||
from plexpy import notifiers
|
from jellypy import notifiers
|
||||||
from plexpy import newsletter_handler
|
from jellypy import newsletter_handler
|
||||||
from plexpy import newsletters
|
from jellypy import newsletters
|
||||||
from plexpy import plextv
|
from jellypy import plextv
|
||||||
from plexpy import users
|
from jellypy import users
|
||||||
|
|
||||||
|
|
||||||
class API2(object):
|
class API2(object):
|
||||||
@@ -107,15 +107,15 @@ class API2(object):
|
|||||||
def _api_validate(self, *args, **kwargs):
|
def _api_validate(self, *args, **kwargs):
|
||||||
""" Sets class vars and remove unneeded parameters. """
|
""" Sets class vars and remove unneeded parameters. """
|
||||||
|
|
||||||
if not plexpy.CONFIG.API_ENABLED:
|
if not jellypy.CONFIG.API_ENABLED:
|
||||||
self._api_msg = 'API not enabled'
|
self._api_msg = 'API not enabled'
|
||||||
self._api_response_code = 404
|
self._api_response_code = 404
|
||||||
|
|
||||||
elif not plexpy.CONFIG.API_KEY:
|
elif not jellypy.CONFIG.API_KEY:
|
||||||
self._api_msg = 'API key not generated'
|
self._api_msg = 'API key not generated'
|
||||||
self._api_response_code = 401
|
self._api_response_code = 401
|
||||||
|
|
||||||
elif len(plexpy.CONFIG.API_KEY) != 32:
|
elif len(jellypy.CONFIG.API_KEY) != 32:
|
||||||
self._api_msg = 'API key not generated correctly'
|
self._api_msg = 'API key not generated correctly'
|
||||||
self._api_response_code = 401
|
self._api_response_code = 401
|
||||||
|
|
||||||
@@ -142,8 +142,8 @@ class API2(object):
|
|||||||
if 'app' in kwargs and helpers.bool_true(kwargs.pop('app')):
|
if 'app' in kwargs and helpers.bool_true(kwargs.pop('app')):
|
||||||
self._api_app = True
|
self._api_app = True
|
||||||
|
|
||||||
if plexpy.CONFIG.API_ENABLED and not self._api_msg or self._api_cmd in ('get_apikey', 'docs', 'docs_md'):
|
if jellypy.CONFIG.API_ENABLED and not self._api_msg or self._api_cmd in ('get_apikey', 'docs', 'docs_md'):
|
||||||
if not self._api_app and self._api_apikey == plexpy.CONFIG.API_KEY:
|
if not self._api_app and self._api_apikey == jellypy.CONFIG.API_KEY:
|
||||||
self._api_authenticated = True
|
self._api_authenticated = True
|
||||||
|
|
||||||
elif self._api_app and self._api_apikey == mobile_app.get_temp_device_token() and \
|
elif self._api_app and self._api_apikey == mobile_app.get_temp_device_token() and \
|
||||||
@@ -203,7 +203,7 @@ class API2(object):
|
|||||||
]
|
]
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
logfile = os.path.join(plexpy.CONFIG.LOG_DIR, logger.FILENAME)
|
logfile = os.path.join(jellypy.CONFIG.LOG_DIR, logger.FILENAME)
|
||||||
templog = []
|
templog = []
|
||||||
start = int(start)
|
start = int(start)
|
||||||
end = int(end)
|
end = int(end)
|
||||||
@@ -290,11 +290,11 @@ class API2(object):
|
|||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
interface_dir = os.path.join(plexpy.PROG_DIR, 'data/interfaces/')
|
interface_dir = os.path.join(jellypy.PROG_DIR, 'data/interfaces/')
|
||||||
interface_list = [name for name in os.listdir(interface_dir) if
|
interface_list = [name for name in os.listdir(interface_dir) if
|
||||||
os.path.isdir(os.path.join(interface_dir, name))]
|
os.path.isdir(os.path.join(interface_dir, name))]
|
||||||
|
|
||||||
conf = plexpy.CONFIG._config
|
conf = jellypy.CONFIG._config
|
||||||
config = {}
|
config = {}
|
||||||
|
|
||||||
# Truthify the dict
|
# Truthify the dict
|
||||||
@@ -332,7 +332,7 @@ class API2(object):
|
|||||||
None
|
None
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
if not plexpy.CONFIG.API_SQL:
|
if not jellypy.CONFIG.API_SQL:
|
||||||
self._api_msg = 'SQL not enabled for the API.'
|
self._api_msg = 'SQL not enabled for the API.'
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -342,12 +342,12 @@ class API2(object):
|
|||||||
|
|
||||||
# allow the user to shoot them self
|
# allow the user to shoot them self
|
||||||
# in the foot but not in the head..
|
# in the foot but not in the head..
|
||||||
if not len(os.listdir(plexpy.CONFIG.BACKUP_DIR)):
|
if not len(os.listdir(jellypy.CONFIG.BACKUP_DIR)):
|
||||||
self.backup_db()
|
self.backup_db()
|
||||||
else:
|
else:
|
||||||
# If the backup is less then 24 h old lets make a backup
|
# If the backup is less then 24 h old lets make a backup
|
||||||
if not any(os.path.getctime(os.path.join(plexpy.CONFIG.BACKUP_DIR, file_)) > (time.time() - 86400)
|
if not any(os.path.getctime(os.path.join(jellypy.CONFIG.BACKUP_DIR, file_)) > (time.time() - 86400)
|
||||||
and file_.endswith('.db') for file_ in os.listdir(plexpy.CONFIG.BACKUP_DIR)):
|
and file_.endswith('.db') for file_ in os.listdir(jellypy.CONFIG.BACKUP_DIR)):
|
||||||
self.backup_db()
|
self.backup_db()
|
||||||
|
|
||||||
db = database.MonitorDatabase()
|
db = database.MonitorDatabase()
|
||||||
@@ -363,7 +363,7 @@ class API2(object):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
def backup_db(self):
|
def backup_db(self):
|
||||||
""" Create a manual backup of the `plexpy.db` file."""
|
""" Create a manual backup of the `jellypy.db` file."""
|
||||||
|
|
||||||
data = database.make_backup()
|
data = database.make_backup()
|
||||||
self._api_result_type = 'success' if data else 'error'
|
self._api_result_type = 'success' if data else 'error'
|
||||||
@@ -373,14 +373,14 @@ class API2(object):
|
|||||||
def restart(self, **kwargs):
|
def restart(self, **kwargs):
|
||||||
""" Restart Tautulli."""
|
""" Restart Tautulli."""
|
||||||
|
|
||||||
plexpy.SIGNAL = 'restart'
|
jellypy.SIGNAL = 'restart'
|
||||||
self._api_msg = 'Restarting Tautulli'
|
self._api_msg = 'Restarting Tautulli'
|
||||||
self._api_result_type = 'success'
|
self._api_result_type = 'success'
|
||||||
|
|
||||||
def update(self, **kwargs):
|
def update(self, **kwargs):
|
||||||
""" Update Tautulli."""
|
""" Update Tautulli."""
|
||||||
|
|
||||||
plexpy.SIGNAL = 'update'
|
jellypy.SIGNAL = 'update'
|
||||||
self._api_msg = 'Updating Tautulli'
|
self._api_msg = 'Updating Tautulli'
|
||||||
self._api_result_type = 'success'
|
self._api_result_type = 'success'
|
||||||
|
|
||||||
@@ -472,9 +472,9 @@ class API2(object):
|
|||||||
mobile_app.set_temp_device_token(True)
|
mobile_app.set_temp_device_token(True)
|
||||||
|
|
||||||
plex_server = plextv.get_server_resources(return_info=True)
|
plex_server = plextv.get_server_resources(return_info=True)
|
||||||
tautulli = plexpy.get_tautulli_info()
|
tautulli = jellypy.get_tautulli_info()
|
||||||
|
|
||||||
data = {"server_id": plexpy.CONFIG.PMS_UUID}
|
data = {"server_id": jellypy.CONFIG.PMS_UUID}
|
||||||
data.update(plex_server)
|
data.update(plex_server)
|
||||||
data.update(tautulli)
|
data.update(tautulli)
|
||||||
|
|
||||||
@@ -646,32 +646,32 @@ General optional parameters:
|
|||||||
"""
|
"""
|
||||||
data = None
|
data = None
|
||||||
apikey = hashlib.sha224(str(random.getrandbits(256)).encode('utf-8')).hexdigest()[0:32]
|
apikey = hashlib.sha224(str(random.getrandbits(256)).encode('utf-8')).hexdigest()[0:32]
|
||||||
if plexpy.CONFIG.HTTP_USERNAME and plexpy.CONFIG.HTTP_PASSWORD:
|
if jellypy.CONFIG.HTTP_USERNAME and jellypy.CONFIG.HTTP_PASSWORD:
|
||||||
authenticated = False
|
authenticated = False
|
||||||
if plexpy.CONFIG.HTTP_HASHED_PASSWORD and \
|
if jellypy.CONFIG.HTTP_HASHED_PASSWORD and \
|
||||||
username == plexpy.CONFIG.HTTP_USERNAME and check_hash(password, plexpy.CONFIG.HTTP_PASSWORD):
|
username == jellypy.CONFIG.HTTP_USERNAME and check_hash(password, jellypy.CONFIG.HTTP_PASSWORD):
|
||||||
authenticated = True
|
authenticated = True
|
||||||
elif not plexpy.CONFIG.HTTP_HASHED_PASSWORD and \
|
elif not jellypy.CONFIG.HTTP_HASHED_PASSWORD and \
|
||||||
username == plexpy.CONFIG.HTTP_USERNAME and password == plexpy.CONFIG.HTTP_PASSWORD:
|
username == jellypy.CONFIG.HTTP_USERNAME and password == jellypy.CONFIG.HTTP_PASSWORD:
|
||||||
authenticated = True
|
authenticated = True
|
||||||
|
|
||||||
if authenticated:
|
if authenticated:
|
||||||
if plexpy.CONFIG.API_KEY:
|
if jellypy.CONFIG.API_KEY:
|
||||||
data = plexpy.CONFIG.API_KEY
|
data = jellypy.CONFIG.API_KEY
|
||||||
else:
|
else:
|
||||||
data = apikey
|
data = apikey
|
||||||
plexpy.CONFIG.API_KEY = apikey
|
jellypy.CONFIG.API_KEY = apikey
|
||||||
plexpy.CONFIG.write()
|
jellypy.CONFIG.write()
|
||||||
else:
|
else:
|
||||||
self._api_msg = 'Authentication is enabled, please add the correct username and password to the parameters'
|
self._api_msg = 'Authentication is enabled, please add the correct username and password to the parameters'
|
||||||
else:
|
else:
|
||||||
if plexpy.CONFIG.API_KEY:
|
if jellypy.CONFIG.API_KEY:
|
||||||
data = plexpy.CONFIG.API_KEY
|
data = jellypy.CONFIG.API_KEY
|
||||||
else:
|
else:
|
||||||
# Make a apikey if the doesn't exist
|
# Make a apikey if the doesn't exist
|
||||||
data = apikey
|
data = apikey
|
||||||
plexpy.CONFIG.API_KEY = apikey
|
jellypy.CONFIG.API_KEY = apikey
|
||||||
plexpy.CONFIG.write()
|
jellypy.CONFIG.write()
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@@ -24,11 +24,11 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from future.moves.urllib.request import FancyURLopener
|
from future.moves.urllib.request import FancyURLopener
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
from common import USER_AGENT
|
from common import USER_AGENT
|
||||||
else:
|
else:
|
||||||
from plexpy.common import USER_AGENT
|
from jellypy.common import USER_AGENT
|
||||||
|
|
||||||
|
|
||||||
class PlexPyURLopener(FancyURLopener):
|
class PlexPyURLopener(FancyURLopener):
|
||||||
@@ -21,11 +21,11 @@ import distro
|
|||||||
import platform
|
import platform
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
import version
|
import version
|
||||||
else:
|
else:
|
||||||
from plexpy import version
|
from jellypy import version
|
||||||
|
|
||||||
|
|
||||||
# Identify Our Application
|
# Identify Our Application
|
||||||
@@ -25,13 +25,13 @@ import threading
|
|||||||
|
|
||||||
from configobj import ConfigObj, ParseError
|
from configobj import ConfigObj, ParseError
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
import helpers
|
import helpers
|
||||||
import logger
|
import logger
|
||||||
else:
|
else:
|
||||||
from plexpy import helpers
|
from jellypy import helpers
|
||||||
from plexpy import logger
|
from jellypy import logger
|
||||||
|
|
||||||
|
|
||||||
def bool_int(value):
|
def bool_int(value):
|
||||||
@@ -257,20 +257,20 @@ def import_tautulli_config(config=None, backup=False):
|
|||||||
# Remove keys that should not be imported
|
# Remove keys that should not be imported
|
||||||
for key in _DO_NOT_IMPORT_KEYS:
|
for key in _DO_NOT_IMPORT_KEYS:
|
||||||
delattr(imported_config, key)
|
delattr(imported_config, key)
|
||||||
if plexpy.DOCKER or plexpy.SNAP:
|
if jellypy.DOCKER or jellypy.SNAP:
|
||||||
for key in _DO_NOT_IMPORT_KEYS_DOCKER:
|
for key in _DO_NOT_IMPORT_KEYS_DOCKER:
|
||||||
delattr(imported_config, key)
|
delattr(imported_config, key)
|
||||||
|
|
||||||
# Merge the imported config file into the current config file
|
# Merge the imported config file into the current config file
|
||||||
plexpy.CONFIG._config.merge(imported_config._config)
|
jellypy.CONFIG._config.merge(imported_config._config)
|
||||||
plexpy.CONFIG.write()
|
jellypy.CONFIG.write()
|
||||||
|
|
||||||
logger.info("Tautulli Config :: Tautulli config import complete.")
|
logger.info("Tautulli Config :: Tautulli config import complete.")
|
||||||
set_import_thread(None)
|
set_import_thread(None)
|
||||||
set_is_importing(False)
|
set_is_importing(False)
|
||||||
|
|
||||||
# Restart to apply changes
|
# Restart to apply changes
|
||||||
plexpy.SIGNAL = 'restart'
|
jellypy.SIGNAL = 'restart'
|
||||||
|
|
||||||
|
|
||||||
def make_backup(cleanup=False, scheduler=False):
|
def make_backup(cleanup=False, scheduler=False):
|
||||||
@@ -280,15 +280,15 @@ def make_backup(cleanup=False, scheduler=False):
|
|||||||
backup_file = 'config.backup-{}.sched.ini'.format(helpers.now())
|
backup_file = 'config.backup-{}.sched.ini'.format(helpers.now())
|
||||||
else:
|
else:
|
||||||
backup_file = 'config.backup-{}.ini'.format(helpers.now())
|
backup_file = 'config.backup-{}.ini'.format(helpers.now())
|
||||||
backup_folder = plexpy.CONFIG.BACKUP_DIR
|
backup_folder = jellypy.CONFIG.BACKUP_DIR
|
||||||
backup_file_fp = os.path.join(backup_folder, backup_file)
|
backup_file_fp = os.path.join(backup_folder, backup_file)
|
||||||
|
|
||||||
# In case the user has deleted it manually
|
# In case the user has deleted it manually
|
||||||
if not os.path.exists(backup_folder):
|
if not os.path.exists(backup_folder):
|
||||||
os.makedirs(backup_folder)
|
os.makedirs(backup_folder)
|
||||||
|
|
||||||
plexpy.CONFIG.write()
|
jellypy.CONFIG.write()
|
||||||
shutil.copyfile(plexpy.CONFIG_FILE, backup_file_fp)
|
shutil.copyfile(jellypy.CONFIG_FILE, backup_file_fp)
|
||||||
|
|
||||||
if cleanup:
|
if cleanup:
|
||||||
now = time.time()
|
now = time.time()
|
||||||
@@ -296,17 +296,17 @@ def make_backup(cleanup=False, scheduler=False):
|
|||||||
for root, dirs, files in os.walk(backup_folder):
|
for root, dirs, files in os.walk(backup_folder):
|
||||||
ini_files = [os.path.join(root, f) for f in files if f.endswith('.sched.ini')]
|
ini_files = [os.path.join(root, f) for f in files if f.endswith('.sched.ini')]
|
||||||
for file_ in ini_files:
|
for file_ in ini_files:
|
||||||
if os.stat(file_).st_mtime < now - plexpy.CONFIG.BACKUP_DAYS * 86400:
|
if os.stat(file_).st_mtime < now - jellypy.CONFIG.BACKUP_DAYS * 86400:
|
||||||
try:
|
try:
|
||||||
os.remove(file_)
|
os.remove(file_)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
logger.error("Tautulli Config :: Failed to delete %s from the backup folder: %s" % (file_, e))
|
logger.error("Tautulli Config :: Failed to delete %s from the backup folder: %s" % (file_, e))
|
||||||
|
|
||||||
if backup_file in os.listdir(backup_folder):
|
if backup_file in os.listdir(backup_folder):
|
||||||
logger.debug("Tautulli Config :: Successfully backed up %s to %s" % (plexpy.CONFIG_FILE, backup_file))
|
logger.debug("Tautulli Config :: Successfully backed up %s to %s" % (jellypy.CONFIG_FILE, backup_file))
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
logger.error("Tautulli Config :: Failed to backup %s to %s" % (plexpy.CONFIG_FILE, backup_file))
|
logger.error("Tautulli Config :: Failed to backup %s to %s" % (jellypy.CONFIG_FILE, backup_file))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -530,7 +530,7 @@ class Config(object):
|
|||||||
self.CONFIG_VERSION = 14
|
self.CONFIG_VERSION = 14
|
||||||
|
|
||||||
if self.CONFIG_VERSION == 14:
|
if self.CONFIG_VERSION == 14:
|
||||||
if plexpy.DOCKER:
|
if jellypy.DOCKER:
|
||||||
self.PLEXPY_AUTO_UPDATE = 0
|
self.PLEXPY_AUTO_UPDATE = 0
|
||||||
|
|
||||||
self.CONFIG_VERSION = 15
|
self.CONFIG_VERSION = 15
|
||||||
@@ -542,7 +542,7 @@ class Config(object):
|
|||||||
self.CONFIG_VERSION = 16
|
self.CONFIG_VERSION = 16
|
||||||
|
|
||||||
if self.CONFIG_VERSION == 16:
|
if self.CONFIG_VERSION == 16:
|
||||||
if plexpy.SNAP:
|
if jellypy.SNAP:
|
||||||
self.PLEXPY_AUTO_UPDATE = 0
|
self.PLEXPY_AUTO_UPDATE = 0
|
||||||
|
|
||||||
self.CONFIG_VERSION = 17
|
self.CONFIG_VERSION = 17
|
||||||
@@ -23,13 +23,13 @@ import shutil
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
import helpers
|
import helpers
|
||||||
import logger
|
import logger
|
||||||
else:
|
else:
|
||||||
from plexpy import helpers
|
from jellypy import helpers
|
||||||
from plexpy import logger
|
from jellypy import logger
|
||||||
|
|
||||||
|
|
||||||
FILENAME = "tautulli.db"
|
FILENAME = "tautulli.db"
|
||||||
@@ -308,7 +308,7 @@ def optimize_db():
|
|||||||
def db_filename(filename=FILENAME):
|
def db_filename(filename=FILENAME):
|
||||||
""" Returns the filepath to the db """
|
""" Returns the filepath to the db """
|
||||||
|
|
||||||
return os.path.join(plexpy.DATA_DIR, filename)
|
return os.path.join(jellypy.DATA_DIR, filename)
|
||||||
|
|
||||||
|
|
||||||
def make_backup(cleanup=False, scheduler=False):
|
def make_backup(cleanup=False, scheduler=False):
|
||||||
@@ -320,13 +320,13 @@ def make_backup(cleanup=False, scheduler=False):
|
|||||||
corrupt = ''
|
corrupt = ''
|
||||||
if not integrity:
|
if not integrity:
|
||||||
corrupt = '.corrupt'
|
corrupt = '.corrupt'
|
||||||
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_plexpydbcorrupt'})
|
jellypy.NOTIFY_QUEUE.put({'notify_action': 'on_plexpydbcorrupt'})
|
||||||
|
|
||||||
if scheduler:
|
if scheduler:
|
||||||
backup_file = 'tautulli.backup-{}{}.sched.db'.format(helpers.now(), corrupt)
|
backup_file = 'tautulli.backup-{}{}.sched.db'.format(helpers.now(), corrupt)
|
||||||
else:
|
else:
|
||||||
backup_file = 'tautulli.backup-{}{}.db'.format(helpers.now(), corrupt)
|
backup_file = 'tautulli.backup-{}{}.db'.format(helpers.now(), corrupt)
|
||||||
backup_folder = plexpy.CONFIG.BACKUP_DIR
|
backup_folder = jellypy.CONFIG.BACKUP_DIR
|
||||||
backup_file_fp = os.path.join(backup_folder, backup_file)
|
backup_file_fp = os.path.join(backup_folder, backup_file)
|
||||||
|
|
||||||
# In case the user has deleted it manually
|
# In case the user has deleted it manually
|
||||||
@@ -345,7 +345,7 @@ def make_backup(cleanup=False, scheduler=False):
|
|||||||
for root, dirs, files in os.walk(backup_folder):
|
for root, dirs, files in os.walk(backup_folder):
|
||||||
db_files = [os.path.join(root, f) for f in files if f.endswith('.sched.db')]
|
db_files = [os.path.join(root, f) for f in files if f.endswith('.sched.db')]
|
||||||
for file_ in db_files:
|
for file_ in db_files:
|
||||||
if os.stat(file_).st_mtime < now - plexpy.CONFIG.BACKUP_DAYS * 86400:
|
if os.stat(file_).st_mtime < now - jellypy.CONFIG.BACKUP_DAYS * 86400:
|
||||||
try:
|
try:
|
||||||
os.remove(file_)
|
os.remove(file_)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
@@ -361,10 +361,10 @@ def make_backup(cleanup=False, scheduler=False):
|
|||||||
|
|
||||||
def get_cache_size():
|
def get_cache_size():
|
||||||
# This will protect against typecasting problems produced by empty string and None settings
|
# This will protect against typecasting problems produced by empty string and None settings
|
||||||
if not plexpy.CONFIG.CACHE_SIZEMB:
|
if not jellypy.CONFIG.CACHE_SIZEMB:
|
||||||
# sqlite will work with this (very slowly)
|
# sqlite will work with this (very slowly)
|
||||||
return 0
|
return 0
|
||||||
return int(plexpy.CONFIG.CACHE_SIZEMB)
|
return int(jellypy.CONFIG.CACHE_SIZEMB)
|
||||||
|
|
||||||
|
|
||||||
def dict_factory(cursor, row):
|
def dict_factory(cursor, row):
|
||||||
@@ -381,9 +381,9 @@ class MonitorDatabase(object):
|
|||||||
self.filename = filename
|
self.filename = filename
|
||||||
self.connection = sqlite3.connect(db_filename(filename), timeout=20)
|
self.connection = sqlite3.connect(db_filename(filename), timeout=20)
|
||||||
# Set database synchronous mode (default NORMAL)
|
# Set database synchronous mode (default NORMAL)
|
||||||
self.connection.execute("PRAGMA synchronous = %s" % plexpy.CONFIG.SYNCHRONOUS_MODE)
|
self.connection.execute("PRAGMA synchronous = %s" % jellypy.CONFIG.SYNCHRONOUS_MODE)
|
||||||
# Set database journal mode (default WAL)
|
# Set database journal mode (default WAL)
|
||||||
self.connection.execute("PRAGMA journal_mode = %s" % plexpy.CONFIG.JOURNAL_MODE)
|
self.connection.execute("PRAGMA journal_mode = %s" % jellypy.CONFIG.JOURNAL_MODE)
|
||||||
# Set database cache size (default 32MB)
|
# Set database cache size (default 32MB)
|
||||||
self.connection.execute("PRAGMA cache_size = -%s" % (get_cache_size() * 1024))
|
self.connection.execute("PRAGMA cache_size = -%s" % (get_cache_size() * 1024))
|
||||||
self.connection.row_factory = dict_factory
|
self.connection.row_factory = dict_factory
|
||||||
@@ -24,8 +24,8 @@ from future.builtins import object
|
|||||||
import json
|
import json
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
import common
|
import common
|
||||||
import database
|
import database
|
||||||
import datatables
|
import datatables
|
||||||
@@ -34,13 +34,13 @@ if plexpy.PYTHON2:
|
|||||||
import pmsconnect
|
import pmsconnect
|
||||||
import session
|
import session
|
||||||
else:
|
else:
|
||||||
from plexpy import common
|
from jellypy import common
|
||||||
from plexpy import database
|
from jellypy import database
|
||||||
from plexpy import datatables
|
from jellypy import datatables
|
||||||
from plexpy import helpers
|
from jellypy import helpers
|
||||||
from plexpy import logger
|
from jellypy import logger
|
||||||
from plexpy import pmsconnect
|
from jellypy import pmsconnect
|
||||||
from plexpy import session
|
from jellypy import session
|
||||||
|
|
||||||
|
|
||||||
class DataFactory(object):
|
class DataFactory(object):
|
||||||
@@ -58,10 +58,10 @@ class DataFactory(object):
|
|||||||
custom_where = []
|
custom_where = []
|
||||||
|
|
||||||
if grouping is None:
|
if grouping is None:
|
||||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
grouping = jellypy.CONFIG.GROUP_HISTORY_TABLES
|
||||||
|
|
||||||
if include_activity is None:
|
if include_activity is None:
|
||||||
include_activity = plexpy.CONFIG.HISTORY_TABLE_ACTIVITY
|
include_activity = jellypy.CONFIG.HISTORY_TABLE_ACTIVITY
|
||||||
|
|
||||||
if session.get_session_user_id():
|
if session.get_session_user_id():
|
||||||
session_user_id = str(session.get_session_user_id())
|
session_user_id = str(session.get_session_user_id())
|
||||||
@@ -218,11 +218,11 @@ class DataFactory(object):
|
|||||||
filter_duration = 0
|
filter_duration = 0
|
||||||
total_duration = self.get_total_duration(custom_where=custom_where)
|
total_duration = self.get_total_duration(custom_where=custom_where)
|
||||||
|
|
||||||
watched_percent = {'movie': plexpy.CONFIG.MOVIE_WATCHED_PERCENT,
|
watched_percent = {'movie': jellypy.CONFIG.MOVIE_WATCHED_PERCENT,
|
||||||
'episode': plexpy.CONFIG.TV_WATCHED_PERCENT,
|
'episode': jellypy.CONFIG.TV_WATCHED_PERCENT,
|
||||||
'track': plexpy.CONFIG.MUSIC_WATCHED_PERCENT,
|
'track': jellypy.CONFIG.MUSIC_WATCHED_PERCENT,
|
||||||
'photo': 0,
|
'photo': 0,
|
||||||
'clip': plexpy.CONFIG.TV_WATCHED_PERCENT
|
'clip': jellypy.CONFIG.TV_WATCHED_PERCENT
|
||||||
}
|
}
|
||||||
|
|
||||||
rows = []
|
rows = []
|
||||||
@@ -309,13 +309,13 @@ class DataFactory(object):
|
|||||||
if stat_id:
|
if stat_id:
|
||||||
stats_cards = [stat_id]
|
stats_cards = [stat_id]
|
||||||
if grouping is None:
|
if grouping is None:
|
||||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
grouping = jellypy.CONFIG.GROUP_HISTORY_TABLES
|
||||||
if stats_cards is None:
|
if stats_cards is None:
|
||||||
stats_cards = plexpy.CONFIG.HOME_STATS_CARDS
|
stats_cards = jellypy.CONFIG.HOME_STATS_CARDS
|
||||||
|
|
||||||
movie_watched_percent = plexpy.CONFIG.MOVIE_WATCHED_PERCENT
|
movie_watched_percent = jellypy.CONFIG.MOVIE_WATCHED_PERCENT
|
||||||
tv_watched_percent = plexpy.CONFIG.TV_WATCHED_PERCENT
|
tv_watched_percent = jellypy.CONFIG.TV_WATCHED_PERCENT
|
||||||
music_watched_percent = plexpy.CONFIG.MUSIC_WATCHED_PERCENT
|
music_watched_percent = jellypy.CONFIG.MUSIC_WATCHED_PERCENT
|
||||||
|
|
||||||
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
||||||
sort_type = 'total_duration' if stats_type == 'duration' else 'total_plays'
|
sort_type = 'total_duration' if stats_type == 'duration' else 'total_plays'
|
||||||
@@ -18,15 +18,15 @@ from future.builtins import object
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
import database
|
import database
|
||||||
import helpers
|
import helpers
|
||||||
import logger
|
import logger
|
||||||
else:
|
else:
|
||||||
from plexpy import database
|
from jellypy import database
|
||||||
from plexpy import helpers
|
from jellypy import helpers
|
||||||
from plexpy import logger
|
from jellypy import logger
|
||||||
|
|
||||||
|
|
||||||
class DataTables(object):
|
class DataTables(object):
|
||||||
@@ -29,8 +29,8 @@ from functools import partial, reduce
|
|||||||
from io import open
|
from io import open
|
||||||
from multiprocessing.dummy import Pool as ThreadPool
|
from multiprocessing.dummy import Pool as ThreadPool
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
import database
|
import database
|
||||||
import datatables
|
import datatables
|
||||||
import helpers
|
import helpers
|
||||||
@@ -38,12 +38,12 @@ if plexpy.PYTHON2:
|
|||||||
import users
|
import users
|
||||||
from plex import Plex
|
from plex import Plex
|
||||||
else:
|
else:
|
||||||
from plexpy import database
|
from jellypy import database
|
||||||
from plexpy import datatables
|
from jellypy import datatables
|
||||||
from plexpy import helpers
|
from jellypy import helpers
|
||||||
from plexpy import logger
|
from jellypy import logger
|
||||||
from plexpy import users
|
from jellypy import users
|
||||||
from plexpy.plex import Plex
|
from jellypy.plex import Plex
|
||||||
|
|
||||||
|
|
||||||
class Export(object):
|
class Export(object):
|
||||||
@@ -1534,9 +1534,9 @@ class Export(object):
|
|||||||
user_tokens = user_data.get_tokens(user_id=self.user_id)
|
user_tokens = user_data.get_tokens(user_id=self.user_id)
|
||||||
plex_token = user_tokens['server_token']
|
plex_token = user_tokens['server_token']
|
||||||
else:
|
else:
|
||||||
plex_token = plexpy.CONFIG.PMS_TOKEN
|
plex_token = jellypy.CONFIG.PMS_TOKEN
|
||||||
|
|
||||||
plex = Plex(plexpy.CONFIG.PMS_URL, plex_token)
|
plex = Plex(jellypy.CONFIG.PMS_URL, plex_token)
|
||||||
|
|
||||||
if self.rating_key:
|
if self.rating_key:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
@@ -1691,7 +1691,7 @@ class Export(object):
|
|||||||
self.total_items = len(items)
|
self.total_items = len(items)
|
||||||
logger.info("Tautulli Exporter :: Exporting %d item(s).", self.total_items)
|
logger.info("Tautulli Exporter :: Exporting %d item(s).", self.total_items)
|
||||||
|
|
||||||
pool = ThreadPool(processes=plexpy.CONFIG.EXPORT_THREADS)
|
pool = ThreadPool(processes=jellypy.CONFIG.EXPORT_THREADS)
|
||||||
items = [ExportObject(self, item) for item in items]
|
items = [ExportObject(self, item) for item in items]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -2107,7 +2107,7 @@ def delete_export(export_id):
|
|||||||
def delete_all_exports():
|
def delete_all_exports():
|
||||||
logger.info("Tautulli Exporter :: Deleting all exports from the export directory.")
|
logger.info("Tautulli Exporter :: Deleting all exports from the export directory.")
|
||||||
|
|
||||||
export_dir = plexpy.CONFIG.EXPORT_DIR
|
export_dir = jellypy.CONFIG.EXPORT_DIR
|
||||||
try:
|
try:
|
||||||
shutil.rmtree(export_dir, ignore_errors=True)
|
shutil.rmtree(export_dir, ignore_errors=True)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
@@ -2233,7 +2233,7 @@ def format_export_filename(title, file_format):
|
|||||||
def get_export_dirpath(title, timestamp=None, images_directory=None):
|
def get_export_dirpath(title, timestamp=None, images_directory=None):
|
||||||
if timestamp:
|
if timestamp:
|
||||||
title = format_export_directory(title, timestamp)
|
title = format_export_directory(title, timestamp)
|
||||||
dirpath = os.path.join(plexpy.CONFIG.EXPORT_DIR, title)
|
dirpath = os.path.join(jellypy.CONFIG.EXPORT_DIR, title)
|
||||||
if images_directory:
|
if images_directory:
|
||||||
dirpath = os.path.join(dirpath, '{}.images'.format(images_directory))
|
dirpath = os.path.join(dirpath, '{}.images'.format(images_directory))
|
||||||
return dirpath
|
return dirpath
|
||||||
@@ -22,19 +22,19 @@ from future.builtins import object
|
|||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
import common
|
import common
|
||||||
import database
|
import database
|
||||||
import logger
|
import logger
|
||||||
import libraries
|
import libraries
|
||||||
import session
|
import session
|
||||||
else:
|
else:
|
||||||
from plexpy import common
|
from jellypy import common
|
||||||
from plexpy import database
|
from jellypy import database
|
||||||
from plexpy import logger
|
from jellypy import logger
|
||||||
from plexpy import libraries
|
from jellypy import libraries
|
||||||
from plexpy import session
|
from jellypy import session
|
||||||
|
|
||||||
|
|
||||||
class Graphs(object):
|
class Graphs(object):
|
||||||
@@ -55,7 +55,7 @@ class Graphs(object):
|
|||||||
user_cond = 'AND session_history.user_id = %s ' % user_id
|
user_cond = 'AND session_history.user_id = %s ' % user_id
|
||||||
|
|
||||||
if grouping is None:
|
if grouping is None:
|
||||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
grouping = jellypy.CONFIG.GROUP_HISTORY_TABLES
|
||||||
|
|
||||||
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
||||||
|
|
||||||
@@ -169,7 +169,7 @@ class Graphs(object):
|
|||||||
user_cond = 'AND session_history.user_id = %s ' % user_id
|
user_cond = 'AND session_history.user_id = %s ' % user_id
|
||||||
|
|
||||||
if grouping is None:
|
if grouping is None:
|
||||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
grouping = jellypy.CONFIG.GROUP_HISTORY_TABLES
|
||||||
|
|
||||||
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
||||||
|
|
||||||
@@ -227,7 +227,7 @@ class Graphs(object):
|
|||||||
logger.warn("Tautulli Graphs :: Unable to execute database query for get_total_plays_per_dayofweek: %s." % e)
|
logger.warn("Tautulli Graphs :: Unable to execute database query for get_total_plays_per_dayofweek: %s." % e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if plexpy.CONFIG.WEEK_START_MONDAY:
|
if jellypy.CONFIG.WEEK_START_MONDAY:
|
||||||
days_list = ['Monday', 'Tuesday', 'Wednesday',
|
days_list = ['Monday', 'Tuesday', 'Wednesday',
|
||||||
'Thursday', 'Friday', 'Saturday', 'Sunday']
|
'Thursday', 'Friday', 'Saturday', 'Sunday']
|
||||||
else:
|
else:
|
||||||
@@ -300,7 +300,7 @@ class Graphs(object):
|
|||||||
user_cond = 'AND session_history.user_id = %s ' % user_id
|
user_cond = 'AND session_history.user_id = %s ' % user_id
|
||||||
|
|
||||||
if grouping is None:
|
if grouping is None:
|
||||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
grouping = jellypy.CONFIG.GROUP_HISTORY_TABLES
|
||||||
|
|
||||||
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
||||||
|
|
||||||
@@ -413,7 +413,7 @@ class Graphs(object):
|
|||||||
user_cond = 'AND session_history.user_id = %s ' % user_id
|
user_cond = 'AND session_history.user_id = %s ' % user_id
|
||||||
|
|
||||||
if grouping is None:
|
if grouping is None:
|
||||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
grouping = jellypy.CONFIG.GROUP_HISTORY_TABLES
|
||||||
|
|
||||||
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
||||||
|
|
||||||
@@ -534,7 +534,7 @@ class Graphs(object):
|
|||||||
user_cond = 'AND session_history.user_id = %s ' % user_id
|
user_cond = 'AND session_history.user_id = %s ' % user_id
|
||||||
|
|
||||||
if grouping is None:
|
if grouping is None:
|
||||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
grouping = jellypy.CONFIG.GROUP_HISTORY_TABLES
|
||||||
|
|
||||||
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
||||||
|
|
||||||
@@ -630,7 +630,7 @@ class Graphs(object):
|
|||||||
user_cond = 'AND session_history.user_id = %s ' % user_id
|
user_cond = 'AND session_history.user_id = %s ' % user_id
|
||||||
|
|
||||||
if grouping is None:
|
if grouping is None:
|
||||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
grouping = jellypy.CONFIG.GROUP_HISTORY_TABLES
|
||||||
|
|
||||||
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
||||||
|
|
||||||
@@ -739,7 +739,7 @@ class Graphs(object):
|
|||||||
user_cond = 'AND session_history.user_id = %s ' % user_id
|
user_cond = 'AND session_history.user_id = %s ' % user_id
|
||||||
|
|
||||||
if grouping is None:
|
if grouping is None:
|
||||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
grouping = jellypy.CONFIG.GROUP_HISTORY_TABLES
|
||||||
|
|
||||||
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
||||||
|
|
||||||
@@ -845,7 +845,7 @@ class Graphs(object):
|
|||||||
user_cond = 'AND session_history.user_id = %s ' % user_id
|
user_cond = 'AND session_history.user_id = %s ' % user_id
|
||||||
|
|
||||||
if grouping is None:
|
if grouping is None:
|
||||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
grouping = jellypy.CONFIG.GROUP_HISTORY_TABLES
|
||||||
|
|
||||||
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
||||||
|
|
||||||
@@ -931,7 +931,7 @@ class Graphs(object):
|
|||||||
user_cond = 'AND session_history.user_id = %s ' % user_id
|
user_cond = 'AND session_history.user_id = %s ' % user_id
|
||||||
|
|
||||||
if grouping is None:
|
if grouping is None:
|
||||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
grouping = jellypy.CONFIG.GROUP_HISTORY_TABLES
|
||||||
|
|
||||||
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
||||||
|
|
||||||
@@ -1041,7 +1041,7 @@ class Graphs(object):
|
|||||||
user_cond = 'AND session_history.user_id = %s ' % user_id
|
user_cond = 'AND session_history.user_id = %s ' % user_id
|
||||||
|
|
||||||
if grouping is None:
|
if grouping is None:
|
||||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
grouping = jellypy.CONFIG.GROUP_HISTORY_TABLES
|
||||||
|
|
||||||
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
||||||
|
|
||||||
@@ -1131,7 +1131,7 @@ class Graphs(object):
|
|||||||
user_cond = 'AND session_history.user_id = %s ' % user_id
|
user_cond = 'AND session_history.user_id = %s ' % user_id
|
||||||
|
|
||||||
if grouping is None:
|
if grouping is None:
|
||||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
grouping = jellypy.CONFIG.GROUP_HISTORY_TABLES
|
||||||
|
|
||||||
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
group_by = 'session_history.reference_id' if grouping else 'session_history.id'
|
||||||
|
|
||||||
@@ -52,17 +52,17 @@ from future.moves.urllib.parse import urlencode
|
|||||||
from xml.dom import minidom
|
from xml.dom import minidom
|
||||||
import xmltodict
|
import xmltodict
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
import common
|
import common
|
||||||
import logger
|
import logger
|
||||||
import request
|
import request
|
||||||
from api2 import API2
|
from api2 import API2
|
||||||
else:
|
else:
|
||||||
from plexpy import common
|
from jellypy import common
|
||||||
from plexpy import logger
|
from jellypy import logger
|
||||||
from plexpy import request
|
from jellypy import request
|
||||||
from plexpy.api2 import API2
|
from jellypy.api2 import API2
|
||||||
|
|
||||||
|
|
||||||
def addtoapi(*dargs, **dkwargs):
|
def addtoapi(*dargs, **dkwargs):
|
||||||
@@ -367,7 +367,7 @@ def replace_all(text, dic, normalize=False):
|
|||||||
else:
|
else:
|
||||||
j = unicodedata.normalize('NFC', j)
|
j = unicodedata.normalize('NFC', j)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
j = unicodedata.normalize('NFC', j.decode(plexpy.SYS_ENCODING, 'replace'))
|
j = unicodedata.normalize('NFC', j.decode(jellypy.SYS_ENCODING, 'replace'))
|
||||||
text = text.replace(i, j)
|
text = text.replace(i, j)
|
||||||
return text
|
return text
|
||||||
|
|
||||||
@@ -479,8 +479,8 @@ def create_https_certificates(ssl_cert, ssl_key):
|
|||||||
serial = timestamp()
|
serial = timestamp()
|
||||||
not_before = 0
|
not_before = 0
|
||||||
not_after = 60 * 60 * 24 * 365 * 10 # ten years
|
not_after = 60 * 60 * 24 * 365 * 10 # ten years
|
||||||
domains = ['DNS:' + d.strip() for d in plexpy.CONFIG.HTTPS_DOMAIN.split(',') if d]
|
domains = ['DNS:' + d.strip() for d in jellypy.CONFIG.HTTPS_DOMAIN.split(',') if d]
|
||||||
ips = ['IP:' + d.strip() for d in plexpy.CONFIG.HTTPS_IP.split(',') if d]
|
ips = ['IP:' + d.strip() for d in jellypy.CONFIG.HTTPS_IP.split(',') if d]
|
||||||
alt_names = ','.join(domains + ips).encode('utf-8')
|
alt_names = ','.join(domains + ips).encode('utf-8')
|
||||||
|
|
||||||
# Create the self-signed Tautulli certificate
|
# Create the self-signed Tautulli certificate
|
||||||
@@ -739,15 +739,15 @@ def anon_url(*url):
|
|||||||
"""
|
"""
|
||||||
Return a URL string consisting of the Anonymous redirect URL and an arbitrary number of values appended.
|
Return a URL string consisting of the Anonymous redirect URL and an arbitrary number of values appended.
|
||||||
"""
|
"""
|
||||||
return '' if None in url else '%s%s' % (plexpy.CONFIG.ANON_REDIRECT, ''.join(str(s) for s in url))
|
return '' if None in url else '%s%s' % (jellypy.CONFIG.ANON_REDIRECT, ''.join(str(s) for s in url))
|
||||||
|
|
||||||
|
|
||||||
def get_img_service(include_self=False):
|
def get_img_service(include_self=False):
|
||||||
if plexpy.CONFIG.NOTIFY_UPLOAD_POSTERS == 1:
|
if jellypy.CONFIG.NOTIFY_UPLOAD_POSTERS == 1:
|
||||||
return 'imgur'
|
return 'imgur'
|
||||||
elif plexpy.CONFIG.NOTIFY_UPLOAD_POSTERS == 2 and include_self:
|
elif jellypy.CONFIG.NOTIFY_UPLOAD_POSTERS == 2 and include_self:
|
||||||
return 'self-hosted'
|
return 'self-hosted'
|
||||||
elif plexpy.CONFIG.NOTIFY_UPLOAD_POSTERS == 3:
|
elif jellypy.CONFIG.NOTIFY_UPLOAD_POSTERS == 3:
|
||||||
return 'cloudinary'
|
return 'cloudinary'
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
@@ -757,11 +757,11 @@ def upload_to_imgur(img_data, img_title='', rating_key='', fallback=''):
|
|||||||
""" Uploads an image to Imgur """
|
""" Uploads an image to Imgur """
|
||||||
img_url = delete_hash = ''
|
img_url = delete_hash = ''
|
||||||
|
|
||||||
if not plexpy.CONFIG.IMGUR_CLIENT_ID:
|
if not jellypy.CONFIG.IMGUR_CLIENT_ID:
|
||||||
logger.error("Tautulli Helpers :: Cannot upload image to Imgur. No Imgur client id specified in the settings.")
|
logger.error("Tautulli Helpers :: Cannot upload image to Imgur. No Imgur client id specified in the settings.")
|
||||||
return img_url, delete_hash
|
return img_url, delete_hash
|
||||||
|
|
||||||
headers = {'Authorization': 'Client-ID %s' % plexpy.CONFIG.IMGUR_CLIENT_ID}
|
headers = {'Authorization': 'Client-ID %s' % jellypy.CONFIG.IMGUR_CLIENT_ID}
|
||||||
data = {'image': base64.b64encode(img_data),
|
data = {'image': base64.b64encode(img_data),
|
||||||
'title': img_title.encode('utf-8'),
|
'title': img_title.encode('utf-8'),
|
||||||
'name': str(rating_key) + '.png',
|
'name': str(rating_key) + '.png',
|
||||||
@@ -789,11 +789,11 @@ def upload_to_imgur(img_data, img_title='', rating_key='', fallback=''):
|
|||||||
|
|
||||||
def delete_from_imgur(delete_hash, img_title='', fallback=''):
|
def delete_from_imgur(delete_hash, img_title='', fallback=''):
|
||||||
""" Deletes an image from Imgur """
|
""" Deletes an image from Imgur """
|
||||||
if not plexpy.CONFIG.IMGUR_CLIENT_ID:
|
if not jellypy.CONFIG.IMGUR_CLIENT_ID:
|
||||||
logger.error("Tautulli Helpers :: Cannot delete image from Imgur. No Imgur client id specified in the settings.")
|
logger.error("Tautulli Helpers :: Cannot delete image from Imgur. No Imgur client id specified in the settings.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
headers = {'Authorization': 'Client-ID %s' % plexpy.CONFIG.IMGUR_CLIENT_ID}
|
headers = {'Authorization': 'Client-ID %s' % jellypy.CONFIG.IMGUR_CLIENT_ID}
|
||||||
|
|
||||||
response, err_msg, req_msg = request.request_response2('https://api.imgur.com/3/image/%s' % delete_hash, 'DELETE',
|
response, err_msg, req_msg = request.request_response2('https://api.imgur.com/3/image/%s' % delete_hash, 'DELETE',
|
||||||
headers=headers)
|
headers=headers)
|
||||||
@@ -813,18 +813,18 @@ def upload_to_cloudinary(img_data, img_title='', rating_key='', fallback=''):
|
|||||||
""" Uploads an image to Cloudinary """
|
""" Uploads an image to Cloudinary """
|
||||||
img_url = ''
|
img_url = ''
|
||||||
|
|
||||||
if not plexpy.CONFIG.CLOUDINARY_CLOUD_NAME or not plexpy.CONFIG.CLOUDINARY_API_KEY or not plexpy.CONFIG.CLOUDINARY_API_SECRET:
|
if not jellypy.CONFIG.CLOUDINARY_CLOUD_NAME or not jellypy.CONFIG.CLOUDINARY_API_KEY or not jellypy.CONFIG.CLOUDINARY_API_SECRET:
|
||||||
logger.error("Tautulli Helpers :: Cannot upload image to Cloudinary. Cloudinary settings not specified in the settings.")
|
logger.error("Tautulli Helpers :: Cannot upload image to Cloudinary. Cloudinary settings not specified in the settings.")
|
||||||
return img_url
|
return img_url
|
||||||
|
|
||||||
cloudinary.config(
|
cloudinary.config(
|
||||||
cloud_name=plexpy.CONFIG.CLOUDINARY_CLOUD_NAME,
|
cloud_name=jellypy.CONFIG.CLOUDINARY_CLOUD_NAME,
|
||||||
api_key=plexpy.CONFIG.CLOUDINARY_API_KEY,
|
api_key=jellypy.CONFIG.CLOUDINARY_API_KEY,
|
||||||
api_secret=plexpy.CONFIG.CLOUDINARY_API_SECRET
|
api_secret=jellypy.CONFIG.CLOUDINARY_API_SECRET
|
||||||
)
|
)
|
||||||
|
|
||||||
# Cloudinary library has very poor support for non-ASCII characters on Python 2
|
# Cloudinary library has very poor support for non-ASCII characters on Python 2
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
_img_title = latinToAscii(img_title, replace=True)
|
_img_title = latinToAscii(img_title, replace=True)
|
||||||
else:
|
else:
|
||||||
_img_title = img_title
|
_img_title = img_title
|
||||||
@@ -844,14 +844,14 @@ def upload_to_cloudinary(img_data, img_title='', rating_key='', fallback=''):
|
|||||||
|
|
||||||
def delete_from_cloudinary(rating_key=None, delete_all=False):
|
def delete_from_cloudinary(rating_key=None, delete_all=False):
|
||||||
""" Deletes an image from Cloudinary """
|
""" Deletes an image from Cloudinary """
|
||||||
if not plexpy.CONFIG.CLOUDINARY_CLOUD_NAME or not plexpy.CONFIG.CLOUDINARY_API_KEY or not plexpy.CONFIG.CLOUDINARY_API_SECRET:
|
if not jellypy.CONFIG.CLOUDINARY_CLOUD_NAME or not jellypy.CONFIG.CLOUDINARY_API_KEY or not jellypy.CONFIG.CLOUDINARY_API_SECRET:
|
||||||
logger.error("Tautulli Helpers :: Cannot delete image from Cloudinary. Cloudinary settings not specified in the settings.")
|
logger.error("Tautulli Helpers :: Cannot delete image from Cloudinary. Cloudinary settings not specified in the settings.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
cloudinary.config(
|
cloudinary.config(
|
||||||
cloud_name=plexpy.CONFIG.CLOUDINARY_CLOUD_NAME,
|
cloud_name=jellypy.CONFIG.CLOUDINARY_CLOUD_NAME,
|
||||||
api_key=plexpy.CONFIG.CLOUDINARY_API_KEY,
|
api_key=jellypy.CONFIG.CLOUDINARY_API_KEY,
|
||||||
api_secret=plexpy.CONFIG.CLOUDINARY_API_SECRET
|
api_secret=jellypy.CONFIG.CLOUDINARY_API_SECRET
|
||||||
)
|
)
|
||||||
|
|
||||||
if delete_all:
|
if delete_all:
|
||||||
@@ -870,14 +870,14 @@ def cloudinary_transform(rating_key=None, width=1000, height=1500, opacity=100,
|
|||||||
img_format='png', img_title='', fallback=None):
|
img_format='png', img_title='', fallback=None):
|
||||||
url = ''
|
url = ''
|
||||||
|
|
||||||
if not plexpy.CONFIG.CLOUDINARY_CLOUD_NAME or not plexpy.CONFIG.CLOUDINARY_API_KEY or not plexpy.CONFIG.CLOUDINARY_API_SECRET:
|
if not jellypy.CONFIG.CLOUDINARY_CLOUD_NAME or not jellypy.CONFIG.CLOUDINARY_API_KEY or not jellypy.CONFIG.CLOUDINARY_API_SECRET:
|
||||||
logger.error("Tautulli Helpers :: Cannot transform image on Cloudinary. Cloudinary settings not specified in the settings.")
|
logger.error("Tautulli Helpers :: Cannot transform image on Cloudinary. Cloudinary settings not specified in the settings.")
|
||||||
return url
|
return url
|
||||||
|
|
||||||
cloudinary.config(
|
cloudinary.config(
|
||||||
cloud_name=plexpy.CONFIG.CLOUDINARY_CLOUD_NAME,
|
cloud_name=jellypy.CONFIG.CLOUDINARY_CLOUD_NAME,
|
||||||
api_key=plexpy.CONFIG.CLOUDINARY_API_KEY,
|
api_key=jellypy.CONFIG.CLOUDINARY_API_KEY,
|
||||||
api_secret=plexpy.CONFIG.CLOUDINARY_API_SECRET
|
api_secret=jellypy.CONFIG.CLOUDINARY_API_SECRET
|
||||||
)
|
)
|
||||||
|
|
||||||
img_options = {'format': img_format,
|
img_options = {'format': img_format,
|
||||||
@@ -914,7 +914,7 @@ def cache_image(url, image=None):
|
|||||||
If no image is provided, tries to return the image from the cache directory.
|
If no image is provided, tries to return the image from the cache directory.
|
||||||
"""
|
"""
|
||||||
# Create image directory if it doesn't exist
|
# Create image directory if it doesn't exist
|
||||||
imgdir = os.path.join(plexpy.CONFIG.CACHE_DIR, 'images/')
|
imgdir = os.path.join(jellypy.CONFIG.CACHE_DIR, 'images/')
|
||||||
if not os.path.exists(imgdir):
|
if not os.path.exists(imgdir):
|
||||||
logger.debug("Tautulli Helpers :: Creating image cache directory at %s" % imgdir)
|
logger.debug("Tautulli Helpers :: Creating image cache directory at %s" % imgdir)
|
||||||
os.makedirs(imgdir)
|
os.makedirs(imgdir)
|
||||||
@@ -1122,12 +1122,12 @@ def eval_logic_groups_to_bool(logic_groups, eval_conds):
|
|||||||
|
|
||||||
|
|
||||||
def get_plexpy_url(hostname=None):
|
def get_plexpy_url(hostname=None):
|
||||||
if plexpy.CONFIG.ENABLE_HTTPS:
|
if jellypy.CONFIG.ENABLE_HTTPS:
|
||||||
scheme = 'https'
|
scheme = 'https'
|
||||||
else:
|
else:
|
||||||
scheme = 'http'
|
scheme = 'http'
|
||||||
|
|
||||||
if hostname is None and plexpy.CONFIG.HTTP_HOST == '0.0.0.0':
|
if hostname is None and jellypy.CONFIG.HTTP_HOST == '0.0.0.0':
|
||||||
import socket
|
import socket
|
||||||
try:
|
try:
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
@@ -1142,18 +1142,18 @@ def get_plexpy_url(hostname=None):
|
|||||||
|
|
||||||
if not hostname:
|
if not hostname:
|
||||||
hostname = 'localhost'
|
hostname = 'localhost'
|
||||||
elif hostname == 'localhost' and plexpy.CONFIG.HTTP_HOST != '0.0.0.0':
|
elif hostname == 'localhost' and jellypy.CONFIG.HTTP_HOST != '0.0.0.0':
|
||||||
hostname = plexpy.CONFIG.HTTP_HOST
|
hostname = jellypy.CONFIG.HTTP_HOST
|
||||||
else:
|
else:
|
||||||
hostname = hostname or plexpy.CONFIG.HTTP_HOST
|
hostname = hostname or jellypy.CONFIG.HTTP_HOST
|
||||||
|
|
||||||
if plexpy.HTTP_PORT not in (80, 443):
|
if jellypy.HTTP_PORT not in (80, 443):
|
||||||
port = ':' + str(plexpy.HTTP_PORT)
|
port = ':' + str(jellypy.HTTP_PORT)
|
||||||
else:
|
else:
|
||||||
port = ''
|
port = ''
|
||||||
|
|
||||||
if plexpy.HTTP_ROOT is not None and plexpy.HTTP_ROOT.strip('/'):
|
if jellypy.HTTP_ROOT is not None and jellypy.HTTP_ROOT.strip('/'):
|
||||||
root = '/' + plexpy.HTTP_ROOT.strip('/')
|
root = '/' + jellypy.HTTP_ROOT.strip('/')
|
||||||
else:
|
else:
|
||||||
root = ''
|
root = ''
|
||||||
|
|
||||||
@@ -1202,10 +1202,10 @@ def split_args(args=None):
|
|||||||
if isinstance(args, list):
|
if isinstance(args, list):
|
||||||
return args
|
return args
|
||||||
elif isinstance(args, str):
|
elif isinstance(args, str):
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
args = args.encode('utf-8')
|
args = args.encode('utf-8')
|
||||||
args = shlex.split(args)
|
args = shlex.split(args)
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
args = [a.decode('utf-8') for a in args]
|
args = [a.decode('utf-8') for a in args]
|
||||||
return args
|
return args
|
||||||
return []
|
return []
|
||||||
@@ -26,13 +26,13 @@ import certifi
|
|||||||
import requests
|
import requests
|
||||||
import urllib3
|
import urllib3
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
import helpers
|
import helpers
|
||||||
import logger
|
import logger
|
||||||
else:
|
else:
|
||||||
from plexpy import helpers
|
from jellypy import helpers
|
||||||
from plexpy import logger
|
from jellypy import logger
|
||||||
|
|
||||||
|
|
||||||
class HTTPHandler(object):
|
class HTTPHandler(object):
|
||||||
@@ -53,14 +53,14 @@ class HTTPHandler(object):
|
|||||||
self.headers = headers
|
self.headers = headers
|
||||||
else:
|
else:
|
||||||
self.headers = {
|
self.headers = {
|
||||||
'X-Plex-Product': plexpy.common.PRODUCT,
|
'X-Plex-Product': jellypy.common.PRODUCT,
|
||||||
'X-Plex-Version': plexpy.common.RELEASE,
|
'X-Plex-Version': jellypy.common.RELEASE,
|
||||||
'X-Plex-Client-Identifier': plexpy.CONFIG.PMS_UUID,
|
'X-Plex-Client-Identifier': jellypy.CONFIG.PMS_UUID,
|
||||||
'X-Plex-Platform': plexpy.common.PLATFORM,
|
'X-Plex-Platform': jellypy.common.PLATFORM,
|
||||||
'X-Plex-Platform-Version': plexpy.common.PLATFORM_RELEASE,
|
'X-Plex-Platform-Version': jellypy.common.PLATFORM_RELEASE,
|
||||||
'X-Plex-Device': '{} {}'.format(plexpy.common.PLATFORM,
|
'X-Plex-Device': '{} {}'.format(jellypy.common.PLATFORM,
|
||||||
plexpy.common.PLATFORM_RELEASE),
|
jellypy.common.PLATFORM_RELEASE),
|
||||||
'X-Plex-Device-Name': plexpy.common.PLATFORM_DEVICE_NAME
|
'X-Plex-Device-Name': jellypy.common.PLATFORM_DEVICE_NAME
|
||||||
}
|
}
|
||||||
|
|
||||||
self.token = token
|
self.token = token
|
||||||
@@ -23,8 +23,8 @@ from future.builtins import object
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
import common
|
import common
|
||||||
import database
|
import database
|
||||||
import datatables
|
import datatables
|
||||||
@@ -36,22 +36,22 @@ if plexpy.PYTHON2:
|
|||||||
import users
|
import users
|
||||||
from plex import Plex
|
from plex import Plex
|
||||||
else:
|
else:
|
||||||
from plexpy import common
|
from jellypy import common
|
||||||
from plexpy import database
|
from jellypy import database
|
||||||
from plexpy import datatables
|
from jellypy import datatables
|
||||||
from plexpy import helpers
|
from jellypy import helpers
|
||||||
from plexpy import logger
|
from jellypy import logger
|
||||||
from plexpy import plextv
|
from jellypy import plextv
|
||||||
from plexpy import pmsconnect
|
from jellypy import pmsconnect
|
||||||
from plexpy import session
|
from jellypy import session
|
||||||
from plexpy import users
|
from jellypy import users
|
||||||
from plexpy.plex import Plex
|
from jellypy.plex import Plex
|
||||||
|
|
||||||
|
|
||||||
def refresh_libraries():
|
def refresh_libraries():
|
||||||
logger.info("Tautulli Libraries :: Requesting libraries list refresh...")
|
logger.info("Tautulli Libraries :: Requesting libraries list refresh...")
|
||||||
|
|
||||||
server_id = plexpy.CONFIG.PMS_IDENTIFIER
|
server_id = jellypy.CONFIG.PMS_IDENTIFIER
|
||||||
if not server_id:
|
if not server_id:
|
||||||
logger.error("Tautulli Libraries :: No PMS identifier, cannot refresh libraries. Verify server in settings.")
|
logger.error("Tautulli Libraries :: No PMS identifier, cannot refresh libraries. Verify server in settings.")
|
||||||
return
|
return
|
||||||
@@ -96,15 +96,15 @@ def refresh_libraries():
|
|||||||
|
|
||||||
query = 'UPDATE library_sections SET is_active = 0 WHERE server_id != ? OR ' \
|
query = 'UPDATE library_sections SET is_active = 0 WHERE server_id != ? OR ' \
|
||||||
'section_id NOT IN ({})'.format(', '.join(['?'] * len(section_ids)))
|
'section_id NOT IN ({})'.format(', '.join(['?'] * len(section_ids)))
|
||||||
monitor_db.action(query=query, args=[plexpy.CONFIG.PMS_IDENTIFIER] + section_ids)
|
monitor_db.action(query=query, args=[jellypy.CONFIG.PMS_IDENTIFIER] + section_ids)
|
||||||
|
|
||||||
if plexpy.CONFIG.HOME_LIBRARY_CARDS == ['first_run_wizard']:
|
if jellypy.CONFIG.HOME_LIBRARY_CARDS == ['first_run_wizard']:
|
||||||
plexpy.CONFIG.__setattr__('HOME_LIBRARY_CARDS', library_keys)
|
jellypy.CONFIG.__setattr__('HOME_LIBRARY_CARDS', library_keys)
|
||||||
plexpy.CONFIG.write()
|
jellypy.CONFIG.write()
|
||||||
else:
|
else:
|
||||||
new_keys = plexpy.CONFIG.HOME_LIBRARY_CARDS + new_keys
|
new_keys = jellypy.CONFIG.HOME_LIBRARY_CARDS + new_keys
|
||||||
plexpy.CONFIG.__setattr__('HOME_LIBRARY_CARDS', new_keys)
|
jellypy.CONFIG.__setattr__('HOME_LIBRARY_CARDS', new_keys)
|
||||||
plexpy.CONFIG.write()
|
jellypy.CONFIG.write()
|
||||||
|
|
||||||
logger.info("Tautulli Libraries :: Libraries list refreshed.")
|
logger.info("Tautulli Libraries :: Libraries list refreshed.")
|
||||||
return True
|
return True
|
||||||
@@ -117,7 +117,7 @@ def add_live_tv_library(refresh=False):
|
|||||||
monitor_db = database.MonitorDatabase()
|
monitor_db = database.MonitorDatabase()
|
||||||
result = monitor_db.select_single('SELECT * FROM library_sections '
|
result = monitor_db.select_single('SELECT * FROM library_sections '
|
||||||
'WHERE section_id = ? and server_id = ?',
|
'WHERE section_id = ? and server_id = ?',
|
||||||
[common.LIVE_TV_SECTION_ID, plexpy.CONFIG.PMS_IDENTIFIER])
|
[common.LIVE_TV_SECTION_ID, jellypy.CONFIG.PMS_IDENTIFIER])
|
||||||
|
|
||||||
if result and not refresh or not result and refresh:
|
if result and not refresh or not result and refresh:
|
||||||
return
|
return
|
||||||
@@ -125,9 +125,9 @@ def add_live_tv_library(refresh=False):
|
|||||||
if not refresh:
|
if not refresh:
|
||||||
logger.info("Tautulli Libraries :: Adding Live TV library to the database.")
|
logger.info("Tautulli Libraries :: Adding Live TV library to the database.")
|
||||||
|
|
||||||
section_keys = {'server_id': plexpy.CONFIG.PMS_IDENTIFIER,
|
section_keys = {'server_id': jellypy.CONFIG.PMS_IDENTIFIER,
|
||||||
'section_id': common.LIVE_TV_SECTION_ID}
|
'section_id': common.LIVE_TV_SECTION_ID}
|
||||||
section_values = {'server_id': plexpy.CONFIG.PMS_IDENTIFIER,
|
section_values = {'server_id': jellypy.CONFIG.PMS_IDENTIFIER,
|
||||||
'section_id': common.LIVE_TV_SECTION_ID,
|
'section_id': common.LIVE_TV_SECTION_ID,
|
||||||
'section_name': common.LIVE_TV_SECTION_NAME,
|
'section_name': common.LIVE_TV_SECTION_NAME,
|
||||||
'section_type': 'live',
|
'section_type': 'live',
|
||||||
@@ -148,7 +148,7 @@ def has_library_type(section_type):
|
|||||||
|
|
||||||
|
|
||||||
def get_collections(section_id=None):
|
def get_collections(section_id=None):
|
||||||
plex = Plex(plexpy.CONFIG.PMS_URL, session.get_session_user_token())
|
plex = Plex(jellypy.CONFIG.PMS_URL, session.get_session_user_token())
|
||||||
library = plex.get_library(section_id)
|
library = plex.get_library(section_id)
|
||||||
|
|
||||||
if library.type not in ('movie', 'show', 'artist'):
|
if library.type not in ('movie', 'show', 'artist'):
|
||||||
@@ -246,7 +246,7 @@ def get_playlists(section_id=None, user_id=None):
|
|||||||
if not plex_token:
|
if not plex_token:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
plex = Plex(plexpy.CONFIG.PMS_URL, plex_token)
|
plex = Plex(jellypy.CONFIG.PMS_URL, plex_token)
|
||||||
|
|
||||||
if user_id:
|
if user_id:
|
||||||
playlists = plex.plex.playlists()
|
playlists = plex.plex.playlists()
|
||||||
@@ -321,7 +321,7 @@ class Libraries(object):
|
|||||||
custom_where = [['library_sections.deleted_section', 0]]
|
custom_where = [['library_sections.deleted_section', 0]]
|
||||||
|
|
||||||
if grouping is None:
|
if grouping is None:
|
||||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
grouping = jellypy.CONFIG.GROUP_HISTORY_TABLES
|
||||||
|
|
||||||
if session.get_session_shared_libraries():
|
if session.get_session_shared_libraries():
|
||||||
custom_where.append(['library_sections.section_id', session.get_session_shared_libraries()])
|
custom_where.append(['library_sections.section_id', session.get_session_shared_libraries()])
|
||||||
@@ -484,7 +484,7 @@ class Libraries(object):
|
|||||||
# Get play counts from the database
|
# Get play counts from the database
|
||||||
monitor_db = database.MonitorDatabase()
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
if plexpy.CONFIG.GROUP_HISTORY_TABLES:
|
if jellypy.CONFIG.GROUP_HISTORY_TABLES:
|
||||||
count_by = 'reference_id'
|
count_by = 'reference_id'
|
||||||
else:
|
else:
|
||||||
count_by = 'id'
|
count_by = 'id'
|
||||||
@@ -517,7 +517,7 @@ class Libraries(object):
|
|||||||
# Import media info cache from json file
|
# Import media info cache from json file
|
||||||
if rating_key:
|
if rating_key:
|
||||||
try:
|
try:
|
||||||
inFilePath = os.path.join(plexpy.CONFIG.CACHE_DIR,'media_info_%s-%s.json' % (section_id, rating_key))
|
inFilePath = os.path.join(jellypy.CONFIG.CACHE_DIR, 'media_info_%s-%s.json' % (section_id, rating_key))
|
||||||
with open(inFilePath, 'r') as inFile:
|
with open(inFilePath, 'r') as inFile:
|
||||||
rows = json.load(inFile)
|
rows = json.load(inFile)
|
||||||
library_count = len(rows)
|
library_count = len(rows)
|
||||||
@@ -527,7 +527,7 @@ class Libraries(object):
|
|||||||
pass
|
pass
|
||||||
elif section_id:
|
elif section_id:
|
||||||
try:
|
try:
|
||||||
inFilePath = os.path.join(plexpy.CONFIG.CACHE_DIR,'media_info_%s.json' % section_id)
|
inFilePath = os.path.join(jellypy.CONFIG.CACHE_DIR, 'media_info_%s.json' % section_id)
|
||||||
with open(inFilePath, 'r') as inFile:
|
with open(inFilePath, 'r') as inFile:
|
||||||
rows = json.load(inFile)
|
rows = json.load(inFile)
|
||||||
library_count = len(rows)
|
library_count = len(rows)
|
||||||
@@ -594,14 +594,14 @@ class Libraries(object):
|
|||||||
# Cache the media info to a json file
|
# Cache the media info to a json file
|
||||||
if rating_key:
|
if rating_key:
|
||||||
try:
|
try:
|
||||||
outFilePath = os.path.join(plexpy.CONFIG.CACHE_DIR,'media_info_%s-%s.json' % (section_id, rating_key))
|
outFilePath = os.path.join(jellypy.CONFIG.CACHE_DIR, 'media_info_%s-%s.json' % (section_id, rating_key))
|
||||||
with open(outFilePath, 'w') as outFile:
|
with open(outFilePath, 'w') as outFile:
|
||||||
json.dump(rows, outFile)
|
json.dump(rows, outFile)
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
logger.debug("Tautulli Libraries :: Unable to create cache file for rating_key %s." % rating_key)
|
logger.debug("Tautulli Libraries :: Unable to create cache file for rating_key %s." % rating_key)
|
||||||
elif section_id:
|
elif section_id:
|
||||||
try:
|
try:
|
||||||
outFilePath = os.path.join(plexpy.CONFIG.CACHE_DIR,'media_info_%s.json' % section_id)
|
outFilePath = os.path.join(jellypy.CONFIG.CACHE_DIR, 'media_info_%s.json' % section_id)
|
||||||
with open(outFilePath, 'w') as outFile:
|
with open(outFilePath, 'w') as outFile:
|
||||||
json.dump(rows, outFile)
|
json.dump(rows, outFile)
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
@@ -694,7 +694,7 @@ class Libraries(object):
|
|||||||
if rating_key:
|
if rating_key:
|
||||||
#logger.debug("Tautulli Libraries :: Getting file sizes for rating_key %s." % rating_key)
|
#logger.debug("Tautulli Libraries :: Getting file sizes for rating_key %s." % rating_key)
|
||||||
try:
|
try:
|
||||||
inFilePath = os.path.join(plexpy.CONFIG.CACHE_DIR,'media_info_%s-%s.json' % (section_id, rating_key))
|
inFilePath = os.path.join(jellypy.CONFIG.CACHE_DIR, 'media_info_%s-%s.json' % (section_id, rating_key))
|
||||||
with open(inFilePath, 'r') as inFile:
|
with open(inFilePath, 'r') as inFile:
|
||||||
rows = json.load(inFile)
|
rows = json.load(inFile)
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
@@ -704,7 +704,7 @@ class Libraries(object):
|
|||||||
elif section_id:
|
elif section_id:
|
||||||
logger.debug("Tautulli Libraries :: Getting file sizes for section_id %s." % section_id)
|
logger.debug("Tautulli Libraries :: Getting file sizes for section_id %s." % section_id)
|
||||||
try:
|
try:
|
||||||
inFilePath = os.path.join(plexpy.CONFIG.CACHE_DIR,'media_info_%s.json' % section_id)
|
inFilePath = os.path.join(jellypy.CONFIG.CACHE_DIR, 'media_info_%s.json' % section_id)
|
||||||
with open(inFilePath, 'r') as inFile:
|
with open(inFilePath, 'r') as inFile:
|
||||||
rows = json.load(inFile)
|
rows = json.load(inFile)
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
@@ -738,14 +738,14 @@ class Libraries(object):
|
|||||||
# Cache the media info to a json file
|
# Cache the media info to a json file
|
||||||
if rating_key:
|
if rating_key:
|
||||||
try:
|
try:
|
||||||
outFilePath = os.path.join(plexpy.CONFIG.CACHE_DIR,'media_info_%s-%s.json' % (section_id, rating_key))
|
outFilePath = os.path.join(jellypy.CONFIG.CACHE_DIR, 'media_info_%s-%s.json' % (section_id, rating_key))
|
||||||
with open(outFilePath, 'w') as outFile:
|
with open(outFilePath, 'w') as outFile:
|
||||||
json.dump(rows, outFile)
|
json.dump(rows, outFile)
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
logger.debug("Tautulli Libraries :: Unable to create cache file with file sizes for rating_key %s." % rating_key)
|
logger.debug("Tautulli Libraries :: Unable to create cache file with file sizes for rating_key %s." % rating_key)
|
||||||
elif section_id:
|
elif section_id:
|
||||||
try:
|
try:
|
||||||
outFilePath = os.path.join(plexpy.CONFIG.CACHE_DIR,'media_info_%s.json' % section_id)
|
outFilePath = os.path.join(jellypy.CONFIG.CACHE_DIR, 'media_info_%s.json' % section_id)
|
||||||
with open(outFilePath, 'w') as outFile:
|
with open(outFilePath, 'w') as outFile:
|
||||||
json.dump(rows, outFile)
|
json.dump(rows, outFile)
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
@@ -796,7 +796,7 @@ class Libraries(object):
|
|||||||
return default_return
|
return default_return
|
||||||
|
|
||||||
if server_id is None:
|
if server_id is None:
|
||||||
server_id = plexpy.CONFIG.PMS_IDENTIFIER
|
server_id = jellypy.CONFIG.PMS_IDENTIFIER
|
||||||
|
|
||||||
def get_library_details(section_id=section_id, server_id=server_id):
|
def get_library_details(section_id=section_id, server_id=server_id):
|
||||||
monitor_db = database.MonitorDatabase()
|
monitor_db = database.MonitorDatabase()
|
||||||
@@ -877,7 +877,7 @@ class Libraries(object):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
if grouping is None:
|
if grouping is None:
|
||||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
grouping = jellypy.CONFIG.GROUP_HISTORY_TABLES
|
||||||
|
|
||||||
if query_days and query_days is not None:
|
if query_days and query_days is not None:
|
||||||
query_days = map(helpers.cast_to_int, query_days.split(','))
|
query_days = map(helpers.cast_to_int, query_days.split(','))
|
||||||
@@ -941,7 +941,7 @@ class Libraries(object):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
if grouping is None:
|
if grouping is None:
|
||||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
grouping = jellypy.CONFIG.GROUP_HISTORY_TABLES
|
||||||
|
|
||||||
monitor_db = database.MonitorDatabase()
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
@@ -1088,13 +1088,13 @@ class Libraries(object):
|
|||||||
return all(success)
|
return all(success)
|
||||||
|
|
||||||
elif str(section_id).isdigit():
|
elif str(section_id).isdigit():
|
||||||
server_id = server_id or plexpy.CONFIG.PMS_IDENTIFIER
|
server_id = server_id or jellypy.CONFIG.PMS_IDENTIFIER
|
||||||
if server_id == plexpy.CONFIG.PMS_IDENTIFIER:
|
if server_id == jellypy.CONFIG.PMS_IDENTIFIER:
|
||||||
delete_success = database.delete_library_history(section_id=section_id)
|
delete_success = database.delete_library_history(section_id=section_id)
|
||||||
else:
|
else:
|
||||||
logger.warn("Tautulli Libraries :: Library history not deleted for library section_id %s "
|
logger.warn("Tautulli Libraries :: Library history not deleted for library section_id %s "
|
||||||
"because library server_id %s does not match Plex server identifier %s."
|
"because library server_id %s does not match Plex server identifier %s."
|
||||||
% (section_id, server_id, plexpy.CONFIG.PMS_IDENTIFIER))
|
% (section_id, server_id, jellypy.CONFIG.PMS_IDENTIFIER))
|
||||||
delete_success = True
|
delete_success = True
|
||||||
|
|
||||||
if purge_only:
|
if purge_only:
|
||||||
@@ -1151,7 +1151,7 @@ class Libraries(object):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if section_id.isdigit():
|
if section_id.isdigit():
|
||||||
[os.remove(os.path.join(plexpy.CONFIG.CACHE_DIR, f)) for f in os.listdir(plexpy.CONFIG.CACHE_DIR)
|
[os.remove(os.path.join(jellypy.CONFIG.CACHE_DIR, f)) for f in os.listdir(jellypy.CONFIG.CACHE_DIR)
|
||||||
if f.startswith('media_info_%s' % section_id) and f.endswith('.json')]
|
if f.startswith('media_info_%s' % section_id) and f.endswith('.json')]
|
||||||
|
|
||||||
logger.debug("Tautulli Libraries :: Deleted media info table cache for section_id %s." % section_id)
|
logger.debug("Tautulli Libraries :: Deleted media info table cache for section_id %s." % section_id)
|
||||||
@@ -1167,7 +1167,7 @@ class Libraries(object):
|
|||||||
# Refresh the PMS_URL to make sure the server_id is updated
|
# Refresh the PMS_URL to make sure the server_id is updated
|
||||||
plextv.get_server_resources()
|
plextv.get_server_resources()
|
||||||
|
|
||||||
server_id = plexpy.CONFIG.PMS_IDENTIFIER
|
server_id = jellypy.CONFIG.PMS_IDENTIFIER
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logger.debug("Tautulli Libraries :: Deleting libraries where server_id does not match %s." % server_id)
|
logger.debug("Tautulli Libraries :: Deleting libraries where server_id does not match %s." % server_id)
|
||||||
@@ -22,11 +22,11 @@ import future.moves.queue as queue
|
|||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
import logger
|
import logger
|
||||||
else:
|
else:
|
||||||
from plexpy import logger
|
from jellypy import logger
|
||||||
|
|
||||||
|
|
||||||
class TimedLock(object):
|
class TimedLock(object):
|
||||||
@@ -20,23 +20,23 @@ from io import open
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
import helpers
|
import helpers
|
||||||
import logger
|
import logger
|
||||||
else:
|
else:
|
||||||
from plexpy import helpers
|
from jellypy import helpers
|
||||||
from plexpy import logger
|
from jellypy import logger
|
||||||
|
|
||||||
|
|
||||||
def get_log_tail(window=20, parsed=True, log_type="server"):
|
def get_log_tail(window=20, parsed=True, log_type="server"):
|
||||||
|
|
||||||
if plexpy.CONFIG.PMS_LOGS_FOLDER:
|
if jellypy.CONFIG.PMS_LOGS_FOLDER:
|
||||||
log_file = ""
|
log_file = ""
|
||||||
if log_type == "server":
|
if log_type == "server":
|
||||||
log_file = os.path.join(plexpy.CONFIG.PMS_LOGS_FOLDER, 'Plex Media Server.log')
|
log_file = os.path.join(jellypy.CONFIG.PMS_LOGS_FOLDER, 'Plex Media Server.log')
|
||||||
elif log_type == "scanner":
|
elif log_type == "scanner":
|
||||||
log_file = os.path.join(plexpy.CONFIG.PMS_LOGS_FOLDER, 'Plex Media Scanner.log')
|
log_file = os.path.join(jellypy.CONFIG.PMS_LOGS_FOLDER, 'Plex Media Scanner.log')
|
||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -32,13 +32,13 @@ import sys
|
|||||||
import threading
|
import threading
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
import helpers
|
import helpers
|
||||||
from config import _BLACKLIST_KEYS, _WHITELIST_KEYS
|
from config import _BLACKLIST_KEYS, _WHITELIST_KEYS
|
||||||
else:
|
else:
|
||||||
from plexpy import helpers
|
from jellypy import helpers
|
||||||
from plexpy.config import _BLACKLIST_KEYS, _WHITELIST_KEYS
|
from jellypy.config import _BLACKLIST_KEYS, _WHITELIST_KEYS
|
||||||
|
|
||||||
|
|
||||||
# These settings are for file logging only
|
# These settings are for file logging only
|
||||||
@@ -96,7 +96,7 @@ class BlacklistFilter(logging.Filter):
|
|||||||
super(BlacklistFilter, self).__init__()
|
super(BlacklistFilter, self).__init__()
|
||||||
|
|
||||||
def filter(self, record):
|
def filter(self, record):
|
||||||
if not plexpy.CONFIG.LOG_BLACKLIST:
|
if not jellypy.CONFIG.LOG_BLACKLIST:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
for item in _BLACKLIST_WORDS:
|
for item in _BLACKLIST_WORDS:
|
||||||
@@ -131,7 +131,7 @@ class RegexFilter(logging.Filter):
|
|||||||
self.regex = re.compile(r'')
|
self.regex = re.compile(r'')
|
||||||
|
|
||||||
def filter(self, record):
|
def filter(self, record):
|
||||||
if not plexpy.CONFIG.LOG_BLACKLIST:
|
if not jellypy.CONFIG.LOG_BLACKLIST:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -349,7 +349,7 @@ def initLogger(console=False, log_dir=False, verbose=False):
|
|||||||
# Add filters to log handlers
|
# Add filters to log handlers
|
||||||
# Only add filters after the config file has been initialized
|
# Only add filters after the config file has been initialized
|
||||||
# Nothing prior to initialization should contain sensitive information
|
# Nothing prior to initialization should contain sensitive information
|
||||||
if not plexpy.DEV and plexpy.CONFIG:
|
if not jellypy.DEV and jellypy.CONFIG:
|
||||||
log_handlers = logger.handlers + \
|
log_handlers = logger.handlers + \
|
||||||
logger_api.handlers + \
|
logger_api.handlers + \
|
||||||
logger_plex_websocket.handlers + \
|
logger_plex_websocket.handlers + \
|
||||||
@@ -30,23 +30,23 @@ except ImportError:
|
|||||||
if HAS_PYOBJC:
|
if HAS_PYOBJC:
|
||||||
import rumps
|
import rumps
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
import common
|
import common
|
||||||
import logger
|
import logger
|
||||||
import versioncheck
|
import versioncheck
|
||||||
else:
|
else:
|
||||||
from plexpy import common
|
from jellypy import common
|
||||||
from plexpy import logger
|
from jellypy import logger
|
||||||
from plexpy import versioncheck
|
from jellypy import versioncheck
|
||||||
|
|
||||||
|
|
||||||
class MacOSSystemTray(object):
|
class MacOSSystemTray(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.image_dir = os.path.join(plexpy.PROG_DIR, 'data/interfaces/', plexpy.CONFIG.INTERFACE, 'images')
|
self.image_dir = os.path.join(jellypy.PROG_DIR, 'data/interfaces/', jellypy.CONFIG.INTERFACE, 'images')
|
||||||
self.icon = os.path.join(self.image_dir, 'logo-flat-white.ico')
|
self.icon = os.path.join(self.image_dir, 'logo-flat-white.ico')
|
||||||
|
|
||||||
if plexpy.UPDATE_AVAILABLE:
|
if jellypy.UPDATE_AVAILABLE:
|
||||||
self.update_title = 'Check for Updates - Update Available!'
|
self.update_title = 'Check for Updates - Update Available!'
|
||||||
else:
|
else:
|
||||||
self.update_title = 'Check for Updates'
|
self.update_title = 'Check for Updates'
|
||||||
@@ -61,10 +61,10 @@ class MacOSSystemTray(object):
|
|||||||
rumps.MenuItem('Restart', callback=self.tray_restart),
|
rumps.MenuItem('Restart', callback=self.tray_restart),
|
||||||
rumps.MenuItem('Quit', callback=self.tray_quit)
|
rumps.MenuItem('Quit', callback=self.tray_quit)
|
||||||
]
|
]
|
||||||
if not plexpy.FROZEN:
|
if not jellypy.FROZEN:
|
||||||
self.menu.insert(6, rumps.MenuItem('Update', callback=self.tray_update))
|
self.menu.insert(6, rumps.MenuItem('Update', callback=self.tray_update))
|
||||||
self.menu[2].state = plexpy.CONFIG.LAUNCH_STARTUP
|
self.menu[2].state = jellypy.CONFIG.LAUNCH_STARTUP
|
||||||
self.menu[3].state = plexpy.CONFIG.LAUNCH_BROWSER
|
self.menu[3].state = jellypy.CONFIG.LAUNCH_BROWSER
|
||||||
|
|
||||||
self.tray_icon = rumps.App(common.PRODUCT, icon=self.icon, template=True,
|
self.tray_icon = rumps.App(common.PRODUCT, icon=self.icon, template=True,
|
||||||
menu=self.menu, quit_button=None)
|
menu=self.menu, quit_button=None)
|
||||||
@@ -84,50 +84,50 @@ class MacOSSystemTray(object):
|
|||||||
self.tray_icon.icon = kwargs['icon']
|
self.tray_icon.icon = kwargs['icon']
|
||||||
|
|
||||||
def tray_open(self, tray_icon):
|
def tray_open(self, tray_icon):
|
||||||
plexpy.launch_browser(plexpy.CONFIG.HTTP_HOST, plexpy.HTTP_PORT, plexpy.HTTP_ROOT)
|
jellypy.launch_browser(jellypy.CONFIG.HTTP_HOST, jellypy.HTTP_PORT, jellypy.HTTP_ROOT)
|
||||||
|
|
||||||
def tray_startup(self, tray_icon):
|
def tray_startup(self, tray_icon):
|
||||||
plexpy.CONFIG.LAUNCH_STARTUP = not plexpy.CONFIG.LAUNCH_STARTUP
|
jellypy.CONFIG.LAUNCH_STARTUP = not jellypy.CONFIG.LAUNCH_STARTUP
|
||||||
set_startup()
|
set_startup()
|
||||||
|
|
||||||
def tray_browser(self, tray_icon):
|
def tray_browser(self, tray_icon):
|
||||||
plexpy.CONFIG.LAUNCH_BROWSER = not plexpy.CONFIG.LAUNCH_BROWSER
|
jellypy.CONFIG.LAUNCH_BROWSER = not jellypy.CONFIG.LAUNCH_BROWSER
|
||||||
set_startup()
|
set_startup()
|
||||||
|
|
||||||
def tray_check_update(self, tray_icon):
|
def tray_check_update(self, tray_icon):
|
||||||
versioncheck.check_update()
|
versioncheck.check_update()
|
||||||
|
|
||||||
def tray_update(self, tray_icon):
|
def tray_update(self, tray_icon):
|
||||||
if plexpy.UPDATE_AVAILABLE:
|
if jellypy.UPDATE_AVAILABLE:
|
||||||
plexpy.SIGNAL = 'update'
|
jellypy.SIGNAL = 'update'
|
||||||
else:
|
else:
|
||||||
self.update_title = 'Check for Updates - No Update Available'
|
self.update_title = 'Check for Updates - No Update Available'
|
||||||
self.menu[5].title = self.update_title
|
self.menu[5].title = self.update_title
|
||||||
|
|
||||||
def tray_restart(self, tray_icon):
|
def tray_restart(self, tray_icon):
|
||||||
plexpy.SIGNAL = 'restart'
|
jellypy.SIGNAL = 'restart'
|
||||||
|
|
||||||
def tray_quit(self, tray_icon):
|
def tray_quit(self, tray_icon):
|
||||||
plexpy.SIGNAL = 'shutdown'
|
jellypy.SIGNAL = 'shutdown'
|
||||||
|
|
||||||
def change_tray_update_icon(self):
|
def change_tray_update_icon(self):
|
||||||
if plexpy.UPDATE_AVAILABLE:
|
if jellypy.UPDATE_AVAILABLE:
|
||||||
self.update_title = 'Check for Updates - Update Available!'
|
self.update_title = 'Check for Updates - Update Available!'
|
||||||
else:
|
else:
|
||||||
self.update_title = 'Check for Updates'
|
self.update_title = 'Check for Updates'
|
||||||
self.menu[5].title = self.update_title
|
self.menu[5].title = self.update_title
|
||||||
|
|
||||||
def change_tray_icons(self):
|
def change_tray_icons(self):
|
||||||
self.tray_icon.menu['Start Tautulli at Login'].state = plexpy.CONFIG.LAUNCH_STARTUP
|
self.tray_icon.menu['Start Tautulli at Login'].state = jellypy.CONFIG.LAUNCH_STARTUP
|
||||||
self.tray_icon.menu['Open Browser when Tautulli Starts'].state = plexpy.CONFIG.LAUNCH_BROWSER
|
self.tray_icon.menu['Open Browser when Tautulli Starts'].state = jellypy.CONFIG.LAUNCH_BROWSER
|
||||||
|
|
||||||
|
|
||||||
def set_startup():
|
def set_startup():
|
||||||
if plexpy.MAC_SYS_TRAY_ICON:
|
if jellypy.MAC_SYS_TRAY_ICON:
|
||||||
plexpy.MAC_SYS_TRAY_ICON.change_tray_icons()
|
jellypy.MAC_SYS_TRAY_ICON.change_tray_icons()
|
||||||
|
|
||||||
if plexpy.INSTALL_TYPE == 'macos':
|
if jellypy.INSTALL_TYPE == 'macos':
|
||||||
if plexpy.CONFIG.LAUNCH_STARTUP:
|
if jellypy.CONFIG.LAUNCH_STARTUP:
|
||||||
try:
|
try:
|
||||||
subprocess.Popen(['osascript',
|
subprocess.Popen(['osascript',
|
||||||
'-e', 'tell application "System Events"',
|
'-e', 'tell application "System Events"',
|
||||||
@@ -162,11 +162,11 @@ def set_startup():
|
|||||||
plist_file_path = os.path.join(launch_agents, plist_file)
|
plist_file_path = os.path.join(launch_agents, plist_file)
|
||||||
|
|
||||||
exe = sys.executable
|
exe = sys.executable
|
||||||
run_args = [arg for arg in plexpy.ARGS if arg != '--nolaunch']
|
run_args = [arg for arg in jellypy.ARGS if arg != '--nolaunch']
|
||||||
if plexpy.FROZEN:
|
if jellypy.FROZEN:
|
||||||
args = [exe] + run_args
|
args = [exe] + run_args
|
||||||
else:
|
else:
|
||||||
args = [exe, plexpy.FULL_PATH] + run_args
|
args = [exe, jellypy.FULL_PATH] + run_args
|
||||||
|
|
||||||
plist_dict = {
|
plist_dict = {
|
||||||
'Label': common.PRODUCT,
|
'Label': common.PRODUCT,
|
||||||
@@ -174,7 +174,7 @@ def set_startup():
|
|||||||
'RunAtLoad': True
|
'RunAtLoad': True
|
||||||
}
|
}
|
||||||
|
|
||||||
if plexpy.CONFIG.LAUNCH_STARTUP:
|
if jellypy.CONFIG.LAUNCH_STARTUP:
|
||||||
if not os.path.exists(launch_agents):
|
if not os.path.exists(launch_agents):
|
||||||
try:
|
try:
|
||||||
os.makedirs(launch_agents)
|
os.makedirs(launch_agents)
|
||||||
@@ -21,15 +21,15 @@ from future.builtins import str
|
|||||||
import requests
|
import requests
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
import database
|
import database
|
||||||
import helpers
|
import helpers
|
||||||
import logger
|
import logger
|
||||||
else:
|
else:
|
||||||
from plexpy import database
|
from jellypy import database
|
||||||
from plexpy import helpers
|
from jellypy import helpers
|
||||||
from plexpy import logger
|
from jellypy import logger
|
||||||
|
|
||||||
|
|
||||||
TEMP_DEVICE_TOKEN = None
|
TEMP_DEVICE_TOKEN = None
|
||||||
@@ -23,17 +23,17 @@ import os
|
|||||||
from apscheduler.triggers.cron import CronTrigger
|
from apscheduler.triggers.cron import CronTrigger
|
||||||
import email.utils
|
import email.utils
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
import database
|
import database
|
||||||
import helpers
|
import helpers
|
||||||
import logger
|
import logger
|
||||||
import newsletters
|
import newsletters
|
||||||
else:
|
else:
|
||||||
from plexpy import database
|
from jellypy import database
|
||||||
from plexpy import helpers
|
from jellypy import helpers
|
||||||
from plexpy import logger
|
from jellypy import logger
|
||||||
from plexpy import newsletters
|
from jellypy import newsletters
|
||||||
|
|
||||||
|
|
||||||
NEWSLETTER_SCHED = None
|
NEWSLETTER_SCHED = None
|
||||||
@@ -48,7 +48,7 @@ def add_newsletter_each(newsletter_id=None, notify_action=None, **kwargs):
|
|||||||
'newsletter_id': newsletter_id,
|
'newsletter_id': newsletter_id,
|
||||||
'notify_action': notify_action}
|
'notify_action': notify_action}
|
||||||
data.update(kwargs)
|
data.update(kwargs)
|
||||||
plexpy.NOTIFY_QUEUE.put(data)
|
jellypy.NOTIFY_QUEUE.put(data)
|
||||||
|
|
||||||
|
|
||||||
def schedule_newsletters(newsletter_id=None):
|
def schedule_newsletters(newsletter_id=None):
|
||||||
@@ -211,7 +211,7 @@ def get_newsletter(newsletter_uuid=None, newsletter_id_name=None):
|
|||||||
end_date.replace('-', ''),
|
end_date.replace('-', ''),
|
||||||
newsletter_uuid)
|
newsletter_uuid)
|
||||||
|
|
||||||
newsletter_folder = plexpy.CONFIG.NEWSLETTER_DIR or os.path.join(plexpy.DATA_DIR, 'newsletters')
|
newsletter_folder = jellypy.CONFIG.NEWSLETTER_DIR or os.path.join(jellypy.DATA_DIR, 'newsletters')
|
||||||
newsletter_file_fp = os.path.join(newsletter_folder, newsletter_file)
|
newsletter_file_fp = os.path.join(newsletter_folder, newsletter_file)
|
||||||
|
|
||||||
if newsletter_file in os.listdir(newsletter_folder):
|
if newsletter_file in os.listdir(newsletter_folder):
|
||||||
@@ -29,8 +29,8 @@ from mako import exceptions
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
import common
|
import common
|
||||||
import database
|
import database
|
||||||
import helpers
|
import helpers
|
||||||
@@ -40,14 +40,14 @@ if plexpy.PYTHON2:
|
|||||||
import pmsconnect
|
import pmsconnect
|
||||||
from notifiers import send_notification, EMAIL
|
from notifiers import send_notification, EMAIL
|
||||||
else:
|
else:
|
||||||
from plexpy import common
|
from jellypy import common
|
||||||
from plexpy import database
|
from jellypy import database
|
||||||
from plexpy import helpers
|
from jellypy import helpers
|
||||||
from plexpy import libraries
|
from jellypy import libraries
|
||||||
from plexpy import logger
|
from jellypy import logger
|
||||||
from plexpy import newsletter_handler
|
from jellypy import newsletter_handler
|
||||||
from plexpy import pmsconnect
|
from jellypy import pmsconnect
|
||||||
from plexpy.notifiers import send_notification, EMAIL
|
from jellypy.notifiers import send_notification, EMAIL
|
||||||
|
|
||||||
|
|
||||||
AGENT_IDS = {
|
AGENT_IDS = {
|
||||||
@@ -319,14 +319,14 @@ def blacklist_logger():
|
|||||||
|
|
||||||
|
|
||||||
def serve_template(templatename, **kwargs):
|
def serve_template(templatename, **kwargs):
|
||||||
if plexpy.CONFIG.NEWSLETTER_CUSTOM_DIR:
|
if jellypy.CONFIG.NEWSLETTER_CUSTOM_DIR:
|
||||||
logger.info("Tautulli Newsletters :: Using custom newsletter template directory.")
|
logger.info("Tautulli Newsletters :: Using custom newsletter template directory.")
|
||||||
template_dir = plexpy.CONFIG.NEWSLETTER_CUSTOM_DIR
|
template_dir = jellypy.CONFIG.NEWSLETTER_CUSTOM_DIR
|
||||||
else:
|
else:
|
||||||
interface_dir = os.path.join(str(plexpy.PROG_DIR), 'data/interfaces/')
|
interface_dir = os.path.join(str(jellypy.PROG_DIR), 'data/interfaces/')
|
||||||
template_dir = os.path.join(str(interface_dir), plexpy.CONFIG.NEWSLETTER_TEMPLATES)
|
template_dir = os.path.join(str(interface_dir), jellypy.CONFIG.NEWSLETTER_TEMPLATES)
|
||||||
|
|
||||||
if not plexpy.CONFIG.NEWSLETTER_INLINE_STYLES:
|
if not jellypy.CONFIG.NEWSLETTER_INLINE_STYLES:
|
||||||
templatename = templatename.replace('.html', '.internal.html')
|
templatename = templatename.replace('.html', '.internal.html')
|
||||||
|
|
||||||
_hplookup = TemplateLookup(directories=[template_dir], default_filters=['unicode', 'h'])
|
_hplookup = TemplateLookup(directories=[template_dir], default_filters=['unicode', 'h'])
|
||||||
@@ -344,7 +344,7 @@ def generate_newsletter_uuid():
|
|||||||
db = database.MonitorDatabase()
|
db = database.MonitorDatabase()
|
||||||
|
|
||||||
while not uuid or uuid_exists:
|
while not uuid or uuid_exists:
|
||||||
uuid = plexpy.generate_uuid()[:8]
|
uuid = jellypy.generate_uuid()[:8]
|
||||||
result = db.select_single(
|
result = db.select_single(
|
||||||
'SELECT EXISTS(SELECT uuid FROM newsletter_log WHERE uuid = ?) as uuid_exists', [uuid])
|
'SELECT EXISTS(SELECT uuid FROM newsletter_log WHERE uuid = ?) as uuid_exists', [uuid])
|
||||||
uuid_exists = result['uuid_exists']
|
uuid_exists = result['uuid_exists']
|
||||||
@@ -528,7 +528,7 @@ class Newsletter(object):
|
|||||||
|
|
||||||
def _save(self):
|
def _save(self):
|
||||||
newsletter_file = self.filename_formatted
|
newsletter_file = self.filename_formatted
|
||||||
newsletter_folder = plexpy.CONFIG.NEWSLETTER_DIR or os.path.join(plexpy.DATA_DIR, 'newsletters')
|
newsletter_folder = jellypy.CONFIG.NEWSLETTER_DIR or os.path.join(jellypy.DATA_DIR, 'newsletters')
|
||||||
newsletter_file_fp = os.path.join(newsletter_folder, newsletter_file)
|
newsletter_file_fp = os.path.join(newsletter_folder, newsletter_file)
|
||||||
|
|
||||||
# In case the user has deleted it manually
|
# In case the user has deleted it manually
|
||||||
@@ -552,7 +552,7 @@ class Newsletter(object):
|
|||||||
newsletter_stripped = ''.join(l.strip() for l in self.newsletter.splitlines())
|
newsletter_stripped = ''.join(l.strip() for l in self.newsletter.splitlines())
|
||||||
|
|
||||||
plaintext = 'HTML email support is required to view the newsletter.\n'
|
plaintext = 'HTML email support is required to view the newsletter.\n'
|
||||||
if plexpy.CONFIG.NEWSLETTER_SELF_HOSTED and plexpy.CONFIG.HTTP_BASE_URL:
|
if jellypy.CONFIG.NEWSLETTER_SELF_HOSTED and jellypy.CONFIG.HTTP_BASE_URL:
|
||||||
plaintext += self._DEFAULT_BODY.format(**self.parameters)
|
plaintext += self._DEFAULT_BODY.format(**self.parameters)
|
||||||
|
|
||||||
email_reply_msg_id = self.email_reply_msg_id if self.config['threaded'] else None
|
email_reply_msg_id = self.email_reply_msg_id if self.config['threaded'] else None
|
||||||
@@ -589,15 +589,15 @@ class Newsletter(object):
|
|||||||
return parameters
|
return parameters
|
||||||
|
|
||||||
def _build_params(self):
|
def _build_params(self):
|
||||||
date_format = helpers.momentjs_to_arrow(plexpy.CONFIG.DATE_FORMAT)
|
date_format = helpers.momentjs_to_arrow(jellypy.CONFIG.DATE_FORMAT)
|
||||||
|
|
||||||
if plexpy.CONFIG.NEWSLETTER_SELF_HOSTED and plexpy.CONFIG.HTTP_BASE_URL:
|
if jellypy.CONFIG.NEWSLETTER_SELF_HOSTED and jellypy.CONFIG.HTTP_BASE_URL:
|
||||||
base_url = plexpy.CONFIG.HTTP_BASE_URL + plexpy.HTTP_ROOT + 'newsletter/'
|
base_url = jellypy.CONFIG.HTTP_BASE_URL + jellypy.HTTP_ROOT + 'newsletter/'
|
||||||
else:
|
else:
|
||||||
base_url = helpers.get_plexpy_url() + '/newsletter/'
|
base_url = helpers.get_plexpy_url() + '/newsletter/'
|
||||||
|
|
||||||
parameters = {
|
parameters = {
|
||||||
'server_name': plexpy.CONFIG.PMS_NAME,
|
'server_name': jellypy.CONFIG.PMS_NAME,
|
||||||
'start_date': self.start_date.format(date_format),
|
'start_date': self.start_date.format(date_format),
|
||||||
'end_date': self.end_date.format(date_format),
|
'end_date': self.end_date.format(date_format),
|
||||||
'current_year': self.start_date.year,
|
'current_year': self.start_date.year,
|
||||||
@@ -616,13 +616,13 @@ class Newsletter(object):
|
|||||||
'newsletter_uuid': self.uuid,
|
'newsletter_uuid': self.uuid,
|
||||||
'newsletter_id': self.newsletter_id,
|
'newsletter_id': self.newsletter_id,
|
||||||
'newsletter_id_name': self.newsletter_id_name,
|
'newsletter_id_name': self.newsletter_id_name,
|
||||||
'newsletter_password': plexpy.CONFIG.NEWSLETTER_PASSWORD
|
'newsletter_password': jellypy.CONFIG.NEWSLETTER_PASSWORD
|
||||||
}
|
}
|
||||||
|
|
||||||
return parameters
|
return parameters
|
||||||
|
|
||||||
def build_text(self):
|
def build_text(self):
|
||||||
from plexpy.notification_handler import CustomFormatter
|
from jellypy.notification_handler import CustomFormatter
|
||||||
custom_formatter = CustomFormatter()
|
custom_formatter = CustomFormatter()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -655,7 +655,7 @@ class Newsletter(object):
|
|||||||
return subject, body, message
|
return subject, body, message
|
||||||
|
|
||||||
def build_filename(self):
|
def build_filename(self):
|
||||||
from plexpy.notification_handler import CustomFormatter
|
from jellypy.notification_handler import CustomFormatter
|
||||||
custom_formatter = CustomFormatter()
|
custom_formatter = CustomFormatter()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -702,7 +702,7 @@ class RecentlyAdded(Newsletter):
|
|||||||
_TEMPLATE = 'recently_added.html'
|
_TEMPLATE = 'recently_added.html'
|
||||||
|
|
||||||
def _get_recently_added(self, media_type=None):
|
def _get_recently_added(self, media_type=None):
|
||||||
from plexpy.notification_handler import format_group_index
|
from jellypy.notification_handler import format_group_index
|
||||||
|
|
||||||
pms_connect = pmsconnect.PmsConnect()
|
pms_connect = pmsconnect.PmsConnect()
|
||||||
|
|
||||||
@@ -818,7 +818,7 @@ class RecentlyAdded(Newsletter):
|
|||||||
return recently_added
|
return recently_added
|
||||||
|
|
||||||
def retrieve_data(self):
|
def retrieve_data(self):
|
||||||
from plexpy.notification_handler import get_img_info, set_hash_image_info
|
from jellypy.notification_handler import get_img_info, set_hash_image_info
|
||||||
|
|
||||||
if not self.config['incl_libraries']:
|
if not self.config['incl_libraries']:
|
||||||
logger.warn("Tautulli Newsletters :: Failed to retrieve %s newsletter data: no libraries selected." % self.NAME)
|
logger.warn("Tautulli Newsletters :: Failed to retrieve %s newsletter data: no libraries selected." % self.NAME)
|
||||||
@@ -948,8 +948,8 @@ class RecentlyAdded(Newsletter):
|
|||||||
newsletter_libraries.append(s['section_name'])
|
newsletter_libraries.append(s['section_name'])
|
||||||
|
|
||||||
parameters['newsletter_libraries'] = ', '.join(sorted(newsletter_libraries))
|
parameters['newsletter_libraries'] = ', '.join(sorted(newsletter_libraries))
|
||||||
parameters['pms_identifier'] = plexpy.CONFIG.PMS_IDENTIFIER
|
parameters['pms_identifier'] = jellypy.CONFIG.PMS_IDENTIFIER
|
||||||
parameters['pms_web_url'] = plexpy.CONFIG.PMS_WEB_URL
|
parameters['pms_web_url'] = jellypy.CONFIG.PMS_WEB_URL
|
||||||
|
|
||||||
return parameters
|
return parameters
|
||||||
|
|
||||||
@@ -39,8 +39,8 @@ import time
|
|||||||
|
|
||||||
import musicbrainzngs
|
import musicbrainzngs
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
import activity_processor
|
import activity_processor
|
||||||
import common
|
import common
|
||||||
import database
|
import database
|
||||||
@@ -52,20 +52,20 @@ if plexpy.PYTHON2:
|
|||||||
import request
|
import request
|
||||||
from newsletter_handler import notify as notify_newsletter
|
from newsletter_handler import notify as notify_newsletter
|
||||||
else:
|
else:
|
||||||
from plexpy import activity_processor
|
from jellypy import activity_processor
|
||||||
from plexpy import common
|
from jellypy import common
|
||||||
from plexpy import database
|
from jellypy import database
|
||||||
from plexpy import datafactory
|
from jellypy import datafactory
|
||||||
from plexpy import logger
|
from jellypy import logger
|
||||||
from plexpy import helpers
|
from jellypy import helpers
|
||||||
from plexpy import notifiers
|
from jellypy import notifiers
|
||||||
from plexpy import pmsconnect
|
from jellypy import pmsconnect
|
||||||
from plexpy import request
|
from jellypy import request
|
||||||
from plexpy.newsletter_handler import notify as notify_newsletter
|
from jellypy.newsletter_handler import notify as notify_newsletter
|
||||||
|
|
||||||
|
|
||||||
def process_queue():
|
def process_queue():
|
||||||
queue = plexpy.NOTIFY_QUEUE
|
queue = jellypy.NOTIFY_QUEUE
|
||||||
while True:
|
while True:
|
||||||
params = queue.get()
|
params = queue.get()
|
||||||
|
|
||||||
@@ -148,14 +148,14 @@ def add_notifier_each(notifier_id=None, notify_action=None, stream_data=None, ti
|
|||||||
'timeline_data': timeline_data,
|
'timeline_data': timeline_data,
|
||||||
'parameters': parameters}
|
'parameters': parameters}
|
||||||
data.update(kwargs)
|
data.update(kwargs)
|
||||||
plexpy.NOTIFY_QUEUE.put(data)
|
jellypy.NOTIFY_QUEUE.put(data)
|
||||||
else:
|
else:
|
||||||
logger.debug("Tautulli NotificationHandler :: Custom notification conditions not satisfied, skipping notifier_id %s." % notifier['id'])
|
logger.debug("Tautulli NotificationHandler :: Custom notification conditions not satisfied, skipping notifier_id %s." % notifier['id'])
|
||||||
|
|
||||||
# Add on_concurrent and on_newdevice to queue if action is on_play
|
# Add on_concurrent and on_newdevice to queue if action is on_play
|
||||||
if notify_action == 'on_play':
|
if notify_action == 'on_play':
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': stream_data.copy(), 'notify_action': 'on_concurrent'})
|
jellypy.NOTIFY_QUEUE.put({'stream_data': stream_data.copy(), 'notify_action': 'on_concurrent'})
|
||||||
plexpy.NOTIFY_QUEUE.put({'stream_data': stream_data.copy(), 'notify_action': 'on_newdevice'})
|
jellypy.NOTIFY_QUEUE.put({'stream_data': stream_data.copy(), 'notify_action': 'on_newdevice'})
|
||||||
|
|
||||||
|
|
||||||
def notify_conditions(notify_action=None, stream_data=None, timeline_data=None):
|
def notify_conditions(notify_action=None, stream_data=None, timeline_data=None):
|
||||||
@@ -186,27 +186,27 @@ def notify_conditions(notify_action=None, stream_data=None, timeline_data=None):
|
|||||||
if result:
|
if result:
|
||||||
user_sessions = [s for s in result['sessions'] if s['user_id'] == stream_data['user_id']]
|
user_sessions = [s for s in result['sessions'] if s['user_id'] == stream_data['user_id']]
|
||||||
|
|
||||||
if plexpy.CONFIG.NOTIFY_CONCURRENT_BY_IP:
|
if jellypy.CONFIG.NOTIFY_CONCURRENT_BY_IP:
|
||||||
evaluated = len(Counter(s['ip_address'] for s in user_sessions)) >= plexpy.CONFIG.NOTIFY_CONCURRENT_THRESHOLD
|
evaluated = len(Counter(s['ip_address'] for s in user_sessions)) >= jellypy.CONFIG.NOTIFY_CONCURRENT_THRESHOLD
|
||||||
else:
|
else:
|
||||||
evaluated = len(user_sessions) >= plexpy.CONFIG.NOTIFY_CONCURRENT_THRESHOLD
|
evaluated = len(user_sessions) >= jellypy.CONFIG.NOTIFY_CONCURRENT_THRESHOLD
|
||||||
|
|
||||||
elif notify_action == 'on_newdevice':
|
elif notify_action == 'on_newdevice':
|
||||||
data_factory = datafactory.DataFactory()
|
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)
|
history_only=not jellypy.CONFIG.NOTIFY_NEW_DEVICE_INITIAL_ONLY)
|
||||||
evaluated = stream_data['machine_id'] not in user_devices
|
evaluated = stream_data['machine_id'] not in user_devices
|
||||||
|
|
||||||
elif stream_data['media_type'] in ('movie', 'episode', 'clip'):
|
elif stream_data['media_type'] in ('movie', 'episode', 'clip'):
|
||||||
progress_percent = helpers.get_percent(stream_data['view_offset'], stream_data['duration'])
|
progress_percent = helpers.get_percent(stream_data['view_offset'], stream_data['duration'])
|
||||||
|
|
||||||
if notify_action == 'on_stop':
|
if notify_action == 'on_stop':
|
||||||
evaluated = (plexpy.CONFIG.NOTIFY_CONSECUTIVE or
|
evaluated = (jellypy.CONFIG.NOTIFY_CONSECUTIVE or
|
||||||
(stream_data['media_type'] == 'movie' and progress_percent < plexpy.CONFIG.MOVIE_WATCHED_PERCENT) or
|
(stream_data['media_type'] == 'movie' and progress_percent < jellypy.CONFIG.MOVIE_WATCHED_PERCENT) or
|
||||||
(stream_data['media_type'] == 'episode' and progress_percent < plexpy.CONFIG.TV_WATCHED_PERCENT))
|
(stream_data['media_type'] == 'episode' and progress_percent < jellypy.CONFIG.TV_WATCHED_PERCENT))
|
||||||
|
|
||||||
elif notify_action == 'on_resume':
|
elif notify_action == 'on_resume':
|
||||||
evaluated = plexpy.CONFIG.NOTIFY_CONSECUTIVE or progress_percent < 99
|
evaluated = jellypy.CONFIG.NOTIFY_CONSECUTIVE or progress_percent < 99
|
||||||
|
|
||||||
# All other activity notify actions
|
# All other activity notify actions
|
||||||
else:
|
else:
|
||||||
@@ -497,9 +497,9 @@ def set_notify_success(notification_id):
|
|||||||
|
|
||||||
def build_media_notify_params(notify_action=None, session=None, timeline=None, manual_trigger=False, **kwargs):
|
def build_media_notify_params(notify_action=None, session=None, timeline=None, manual_trigger=False, **kwargs):
|
||||||
# Get time formats
|
# Get time formats
|
||||||
date_format = helpers.momentjs_to_arrow(plexpy.CONFIG.DATE_FORMAT)
|
date_format = helpers.momentjs_to_arrow(jellypy.CONFIG.DATE_FORMAT)
|
||||||
time_format = helpers.momentjs_to_arrow(plexpy.CONFIG.TIME_FORMAT)
|
time_format = helpers.momentjs_to_arrow(jellypy.CONFIG.TIME_FORMAT)
|
||||||
duration_format = helpers.momentjs_to_arrow(plexpy.CONFIG.TIME_FORMAT, duration=True)
|
duration_format = helpers.momentjs_to_arrow(jellypy.CONFIG.TIME_FORMAT, duration=True)
|
||||||
|
|
||||||
# Get metadata for the item
|
# Get metadata for the item
|
||||||
if session:
|
if session:
|
||||||
@@ -598,8 +598,8 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||||||
plex_web_rating_key = notify_params['rating_key']
|
plex_web_rating_key = notify_params['rating_key']
|
||||||
|
|
||||||
notify_params['plex_url'] = '{web_url}#!/server/{pms_identifier}/details?key=%2Flibrary%2Fmetadata%2F{rating_key}'.format(
|
notify_params['plex_url'] = '{web_url}#!/server/{pms_identifier}/details?key=%2Flibrary%2Fmetadata%2F{rating_key}'.format(
|
||||||
web_url=plexpy.CONFIG.PMS_WEB_URL,
|
web_url=jellypy.CONFIG.PMS_WEB_URL,
|
||||||
pms_identifier=plexpy.CONFIG.PMS_IDENTIFIER,
|
pms_identifier=jellypy.CONFIG.PMS_IDENTIFIER,
|
||||||
rating_key=plex_web_rating_key)
|
rating_key=plex_web_rating_key)
|
||||||
|
|
||||||
# Check external guids
|
# Check external guids
|
||||||
@@ -646,7 +646,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||||||
notify_params['lastfm_url'] = 'https://www.last.fm/music/' + notify_params['lastfm_id']
|
notify_params['lastfm_url'] = 'https://www.last.fm/music/' + notify_params['lastfm_id']
|
||||||
|
|
||||||
# Get TheMovieDB info (for movies and tv only)
|
# Get TheMovieDB info (for movies and tv only)
|
||||||
if plexpy.CONFIG.THEMOVIEDB_LOOKUP and notify_params['media_type'] in ('movie', 'show', 'season', 'episode'):
|
if jellypy.CONFIG.THEMOVIEDB_LOOKUP and notify_params['media_type'] in ('movie', 'show', 'season', 'episode'):
|
||||||
if notify_params.get('themoviedb_id'):
|
if notify_params.get('themoviedb_id'):
|
||||||
themoveidb_json = get_themoviedb_info(rating_key=rating_key,
|
themoveidb_json = get_themoviedb_info(rating_key=rating_key,
|
||||||
media_type=notify_params['media_type'],
|
media_type=notify_params['media_type'],
|
||||||
@@ -689,7 +689,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||||||
notify_params['themoviedb_id'], 'show' if lookup_media_type == 'tv' else 'movie')
|
notify_params['themoviedb_id'], 'show' if lookup_media_type == 'tv' else 'movie')
|
||||||
|
|
||||||
# Get TVmaze info (for tv shows only)
|
# Get TVmaze info (for tv shows only)
|
||||||
if plexpy.CONFIG.TVMAZE_LOOKUP and notify_params['media_type'] in ('show', 'season', 'episode'):
|
if jellypy.CONFIG.TVMAZE_LOOKUP and notify_params['media_type'] in ('show', 'season', 'episode'):
|
||||||
if notify_params.get('thetvdb_id') or notify_params.get('imdb_id') or notify_params.get('plex_id'):
|
if notify_params.get('thetvdb_id') or notify_params.get('imdb_id') or notify_params.get('plex_id'):
|
||||||
if notify_params['media_type'] == 'episode':
|
if notify_params['media_type'] == 'episode':
|
||||||
lookup_key = notify_params['grandparent_rating_key']
|
lookup_key = notify_params['grandparent_rating_key']
|
||||||
@@ -716,7 +716,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||||||
notify_params['trakt_url'] = 'https://trakt.tv/search/imdb/' + notify_params['imdb_id']
|
notify_params['trakt_url'] = 'https://trakt.tv/search/imdb/' + notify_params['imdb_id']
|
||||||
|
|
||||||
# Get MusicBrainz info (for music only)
|
# Get MusicBrainz info (for music only)
|
||||||
if plexpy.CONFIG.MUSICBRAINZ_LOOKUP and notify_params['media_type'] in ('artist', 'album', 'track'):
|
if jellypy.CONFIG.MUSICBRAINZ_LOOKUP and notify_params['media_type'] in ('artist', 'album', 'track'):
|
||||||
artist = release = recording = tracks = tnum = None
|
artist = release = recording = tracks = tnum = None
|
||||||
if notify_params['media_type'] == 'artist':
|
if notify_params['media_type'] == 'artist':
|
||||||
musicbrainz_type = 'artist'
|
musicbrainz_type = 'artist'
|
||||||
@@ -772,13 +772,13 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||||||
img_info = get_img_info(img=poster_thumb, rating_key=poster_key, title=poster_title, fallback=fallback)
|
img_info = get_img_info(img=poster_thumb, rating_key=poster_key, title=poster_title, fallback=fallback)
|
||||||
poster_info = {'poster_title': img_info['img_title'], 'poster_url': img_info['img_url']}
|
poster_info = {'poster_title': img_info['img_title'], 'poster_url': img_info['img_url']}
|
||||||
notify_params.update(poster_info)
|
notify_params.update(poster_info)
|
||||||
elif img_service == 'self-hosted' and plexpy.CONFIG.HTTP_BASE_URL:
|
elif img_service == 'self-hosted' and jellypy.CONFIG.HTTP_BASE_URL:
|
||||||
img_hash = set_hash_image_info(img=poster_thumb, fallback=fallback)
|
img_hash = set_hash_image_info(img=poster_thumb, fallback=fallback)
|
||||||
poster_info = {'poster_title': poster_title,
|
poster_info = {'poster_title': poster_title,
|
||||||
'poster_url': plexpy.CONFIG.HTTP_BASE_URL + plexpy.HTTP_ROOT + 'image/' + img_hash}
|
'poster_url': jellypy.CONFIG.HTTP_BASE_URL + jellypy.HTTP_ROOT + 'image/' + img_hash}
|
||||||
notify_params.update(poster_info)
|
notify_params.update(poster_info)
|
||||||
|
|
||||||
if ((manual_trigger or plexpy.CONFIG.NOTIFY_GROUP_RECENTLY_ADDED_GRANDPARENT)
|
if ((manual_trigger or jellypy.CONFIG.NOTIFY_GROUP_RECENTLY_ADDED_GRANDPARENT)
|
||||||
and notify_params['media_type'] in ('show', 'artist')):
|
and notify_params['media_type'] in ('show', 'artist')):
|
||||||
show_name = notify_params['title']
|
show_name = notify_params['title']
|
||||||
episode_name = ''
|
episode_name = ''
|
||||||
@@ -797,7 +797,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||||||
child_count = len(child_num)
|
child_count = len(child_num)
|
||||||
grandchild_count = ''
|
grandchild_count = ''
|
||||||
|
|
||||||
elif ((manual_trigger or plexpy.CONFIG.NOTIFY_GROUP_RECENTLY_ADDED_PARENT)
|
elif ((manual_trigger or jellypy.CONFIG.NOTIFY_GROUP_RECENTLY_ADDED_PARENT)
|
||||||
and notify_params['media_type'] in ('season', 'album')):
|
and notify_params['media_type'] in ('season', 'album')):
|
||||||
show_name = notify_params['parent_title']
|
show_name = notify_params['parent_title']
|
||||||
episode_name = ''
|
episode_name = ''
|
||||||
@@ -850,16 +850,16 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||||||
available_params = {
|
available_params = {
|
||||||
# Global paramaters
|
# Global paramaters
|
||||||
'tautulli_version': common.RELEASE,
|
'tautulli_version': common.RELEASE,
|
||||||
'tautulli_remote': plexpy.CONFIG.GIT_REMOTE,
|
'tautulli_remote': jellypy.CONFIG.GIT_REMOTE,
|
||||||
'tautulli_branch': plexpy.CONFIG.GIT_BRANCH,
|
'tautulli_branch': jellypy.CONFIG.GIT_BRANCH,
|
||||||
'tautulli_commit': plexpy.CURRENT_VERSION,
|
'tautulli_commit': jellypy.CURRENT_VERSION,
|
||||||
'server_name': plexpy.CONFIG.PMS_NAME,
|
'server_name': jellypy.CONFIG.PMS_NAME,
|
||||||
'server_ip': plexpy.CONFIG.PMS_IP,
|
'server_ip': jellypy.CONFIG.PMS_IP,
|
||||||
'server_port': plexpy.CONFIG.PMS_PORT,
|
'server_port': jellypy.CONFIG.PMS_PORT,
|
||||||
'server_url': plexpy.CONFIG.PMS_URL,
|
'server_url': jellypy.CONFIG.PMS_URL,
|
||||||
'server_machine_id': plexpy.CONFIG.PMS_IDENTIFIER,
|
'server_machine_id': jellypy.CONFIG.PMS_IDENTIFIER,
|
||||||
'server_platform': plexpy.CONFIG.PMS_PLATFORM,
|
'server_platform': jellypy.CONFIG.PMS_PLATFORM,
|
||||||
'server_version': plexpy.CONFIG.PMS_VERSION,
|
'server_version': jellypy.CONFIG.PMS_VERSION,
|
||||||
'action': notify_action.split('on_')[-1],
|
'action': notify_action.split('on_')[-1],
|
||||||
'current_year': now.year,
|
'current_year': now.year,
|
||||||
'current_month': now.month,
|
'current_month': now.month,
|
||||||
@@ -1096,8 +1096,8 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
|||||||
|
|
||||||
def build_server_notify_params(notify_action=None, **kwargs):
|
def build_server_notify_params(notify_action=None, **kwargs):
|
||||||
# Get time formats
|
# Get time formats
|
||||||
date_format = plexpy.CONFIG.DATE_FORMAT.replace('Do','')
|
date_format = jellypy.CONFIG.DATE_FORMAT.replace('Do', '')
|
||||||
time_format = plexpy.CONFIG.TIME_FORMAT.replace('Do','')
|
time_format = jellypy.CONFIG.TIME_FORMAT.replace('Do', '')
|
||||||
|
|
||||||
update_channel = pmsconnect.PmsConnect().get_server_update_channel()
|
update_channel = pmsconnect.PmsConnect().get_server_update_channel()
|
||||||
|
|
||||||
@@ -1111,16 +1111,16 @@ def build_server_notify_params(notify_action=None, **kwargs):
|
|||||||
available_params = {
|
available_params = {
|
||||||
# Global paramaters
|
# Global paramaters
|
||||||
'tautulli_version': common.RELEASE,
|
'tautulli_version': common.RELEASE,
|
||||||
'tautulli_remote': plexpy.CONFIG.GIT_REMOTE,
|
'tautulli_remote': jellypy.CONFIG.GIT_REMOTE,
|
||||||
'tautulli_branch': plexpy.CONFIG.GIT_BRANCH,
|
'tautulli_branch': jellypy.CONFIG.GIT_BRANCH,
|
||||||
'tautulli_commit': plexpy.CURRENT_VERSION,
|
'tautulli_commit': jellypy.CURRENT_VERSION,
|
||||||
'server_name': plexpy.CONFIG.PMS_NAME,
|
'server_name': jellypy.CONFIG.PMS_NAME,
|
||||||
'server_ip': plexpy.CONFIG.PMS_IP,
|
'server_ip': jellypy.CONFIG.PMS_IP,
|
||||||
'server_port': plexpy.CONFIG.PMS_PORT,
|
'server_port': jellypy.CONFIG.PMS_PORT,
|
||||||
'server_url': plexpy.CONFIG.PMS_URL,
|
'server_url': jellypy.CONFIG.PMS_URL,
|
||||||
'server_platform': plexpy.CONFIG.PMS_PLATFORM,
|
'server_platform': jellypy.CONFIG.PMS_PLATFORM,
|
||||||
'server_version': plexpy.CONFIG.PMS_VERSION,
|
'server_version': jellypy.CONFIG.PMS_VERSION,
|
||||||
'server_machine_id': plexpy.CONFIG.PMS_IDENTIFIER,
|
'server_machine_id': jellypy.CONFIG.PMS_IDENTIFIER,
|
||||||
'action': notify_action.split('on_')[-1],
|
'action': notify_action.split('on_')[-1],
|
||||||
'current_year': now.year,
|
'current_year': now.year,
|
||||||
'current_month': now.month,
|
'current_month': now.month,
|
||||||
@@ -1464,7 +1464,7 @@ def set_hash_image_info(img=None, rating_key=None, width=750, height=1000,
|
|||||||
rating_key = img_rating_key
|
rating_key = img_rating_key
|
||||||
|
|
||||||
img_string = '{}.{}.{}.{}.{}.{}.{}.{}'.format(
|
img_string = '{}.{}.{}.{}.{}.{}.{}.{}'.format(
|
||||||
plexpy.CONFIG.PMS_UUID, img, rating_key, width, height, opacity, background, blur, fallback)
|
jellypy.CONFIG.PMS_UUID, img, rating_key, width, height, opacity, background, blur, fallback)
|
||||||
img_hash = hashlib.sha256(img_string.encode('utf-8')).hexdigest()
|
img_hash = hashlib.sha256(img_string.encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
if add_to_db:
|
if add_to_db:
|
||||||
@@ -1572,7 +1572,7 @@ def lookup_themoviedb_by_id(rating_key=None, thetvdb_id=None, imdb_id=None, titl
|
|||||||
else:
|
else:
|
||||||
logger.debug("Tautulli NotificationHandler :: Looking up The Movie Database info for '{} ({})'.".format(title, year))
|
logger.debug("Tautulli NotificationHandler :: Looking up The Movie Database info for '{} ({})'.".format(title, year))
|
||||||
|
|
||||||
params = {'api_key': plexpy.CONFIG.THEMOVIEDB_APIKEY}
|
params = {'api_key': jellypy.CONFIG.THEMOVIEDB_APIKEY}
|
||||||
|
|
||||||
if thetvdb_id or imdb_id:
|
if thetvdb_id or imdb_id:
|
||||||
params['external_source'] = 'tvdb_id' if thetvdb_id else 'imdb_id'
|
params['external_source'] = 'tvdb_id' if thetvdb_id else 'imdb_id'
|
||||||
@@ -1650,7 +1650,7 @@ def get_themoviedb_info(rating_key=None, media_type=None, themoviedb_id=None):
|
|||||||
|
|
||||||
logger.debug("Tautulli NotificationHandler :: Looking up The Movie Database info for themoviedb_id '{}'.".format(themoviedb_id))
|
logger.debug("Tautulli NotificationHandler :: Looking up The Movie Database info for themoviedb_id '{}'.".format(themoviedb_id))
|
||||||
|
|
||||||
params = {'api_key': plexpy.CONFIG.THEMOVIEDB_APIKEY}
|
params = {'api_key': jellypy.CONFIG.THEMOVIEDB_APIKEY}
|
||||||
response, err_msg, req_msg = request.request_response2('https://api.themoviedb.org/3/{}/{}'.format(media_type, themoviedb_id), params=params)
|
response, err_msg, req_msg = request.request_response2('https://api.themoviedb.org/3/{}/{}'.format(media_type, themoviedb_id), params=params)
|
||||||
|
|
||||||
if response and not err_msg:
|
if response and not err_msg:
|
||||||
@@ -1870,7 +1870,7 @@ class CustomFormatter(Formatter):
|
|||||||
obj = self.convert_field(obj, conversion)
|
obj = self.convert_field(obj, conversion)
|
||||||
|
|
||||||
# expand the format spec, if needed
|
# expand the format spec, if needed
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
format_spec = self._vformat(format_spec, args, kwargs,
|
format_spec = self._vformat(format_spec, args, kwargs,
|
||||||
used_args, recursion_depth - 1)
|
used_args, recursion_depth - 1)
|
||||||
else:
|
else:
|
||||||
@@ -1889,7 +1889,7 @@ class CustomFormatter(Formatter):
|
|||||||
result.append(suffix)
|
result.append(suffix)
|
||||||
# result.append(self.format_field(obj, format_spec))
|
# result.append(self.format_field(obj, format_spec))
|
||||||
|
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
return ''.join(result)
|
return ''.join(result)
|
||||||
else:
|
else:
|
||||||
return ''.join(result), auto_arg_index
|
return ''.join(result), auto_arg_index
|
||||||
@@ -57,8 +57,8 @@ import gntp.notifier
|
|||||||
import facebook
|
import facebook
|
||||||
import twitter
|
import twitter
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
import common
|
import common
|
||||||
import database
|
import database
|
||||||
import helpers
|
import helpers
|
||||||
@@ -68,14 +68,14 @@ if plexpy.PYTHON2:
|
|||||||
import request
|
import request
|
||||||
import users
|
import users
|
||||||
else:
|
else:
|
||||||
from plexpy import common
|
from jellypy import common
|
||||||
from plexpy import database
|
from jellypy import database
|
||||||
from plexpy import helpers
|
from jellypy import helpers
|
||||||
from plexpy import logger
|
from jellypy import logger
|
||||||
from plexpy import mobile_app
|
from jellypy import mobile_app
|
||||||
from plexpy import pmsconnect
|
from jellypy import pmsconnect
|
||||||
from plexpy import request
|
from jellypy import request
|
||||||
from plexpy import users
|
from jellypy import users
|
||||||
|
|
||||||
|
|
||||||
BROWSER_NOTIFIERS = {}
|
BROWSER_NOTIFIERS = {}
|
||||||
@@ -943,7 +943,7 @@ class ANDROIDAPP(Notifier):
|
|||||||
'cipher_text': base64.b64encode(encrypted_data),
|
'cipher_text': base64.b64encode(encrypted_data),
|
||||||
'nonce': base64.b64encode(nonce),
|
'nonce': base64.b64encode(nonce),
|
||||||
'salt': base64.b64encode(salt),
|
'salt': base64.b64encode(salt),
|
||||||
'server_id': plexpy.CONFIG.PMS_UUID}
|
'server_id': jellypy.CONFIG.PMS_UUID}
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
logger.warn("Tautulli Notifiers :: PyCryptodome library is missing. "
|
logger.warn("Tautulli Notifiers :: PyCryptodome library is missing. "
|
||||||
@@ -955,7 +955,7 @@ class ANDROIDAPP(Notifier):
|
|||||||
'contents': {'en': 'Tautulli Notification'},
|
'contents': {'en': 'Tautulli Notification'},
|
||||||
'data': {'encrypted': False,
|
'data': {'encrypted': False,
|
||||||
'plain_text': plaintext_data,
|
'plain_text': plaintext_data,
|
||||||
'server_id': plexpy.CONFIG.PMS_UUID}
|
'server_id': jellypy.CONFIG.PMS_UUID}
|
||||||
}
|
}
|
||||||
|
|
||||||
#logger.debug("OneSignal payload: {}".format(payload))
|
#logger.debug("OneSignal payload: {}".format(payload))
|
||||||
@@ -996,7 +996,7 @@ class ANDROIDAPP(Notifier):
|
|||||||
'Instructions can be found in the '
|
'Instructions can be found in the '
|
||||||
'<a href="' + helpers.anon_url(
|
'<a href="' + helpers.anon_url(
|
||||||
'https://github.com/%s/%s-Wiki/wiki/Frequently-Asked-Questions#notifications-pycryptodome'
|
'https://github.com/%s/%s-Wiki/wiki/Frequently-Asked-Questions#notifications-pycryptodome'
|
||||||
% (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO)) + '" target="_blank">FAQ</a>.' ,
|
% (jellypy.CONFIG.GIT_USER, jellypy.CONFIG.GIT_REPO)) + '" target="_blank">FAQ</a>.' ,
|
||||||
'input_type': 'help'
|
'input_type': 'help'
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
@@ -1532,10 +1532,10 @@ class FACEBOOK(Notifier):
|
|||||||
def _get_authorization(self, app_id='', app_secret='', redirect_uri=''):
|
def _get_authorization(self, app_id='', app_secret='', redirect_uri=''):
|
||||||
# Temporarily store settings in the config so we can retrieve them in Facebook step 2.
|
# Temporarily store settings in the config so we can retrieve them in Facebook step 2.
|
||||||
# Assume the user won't be requesting authorization for multiple Facebook notifiers at the same time.
|
# Assume the user won't be requesting authorization for multiple Facebook notifiers at the same time.
|
||||||
plexpy.CONFIG.FACEBOOK_APP_ID = app_id
|
jellypy.CONFIG.FACEBOOK_APP_ID = app_id
|
||||||
plexpy.CONFIG.FACEBOOK_APP_SECRET = app_secret
|
jellypy.CONFIG.FACEBOOK_APP_SECRET = app_secret
|
||||||
plexpy.CONFIG.FACEBOOK_REDIRECT_URI = redirect_uri
|
jellypy.CONFIG.FACEBOOK_REDIRECT_URI = redirect_uri
|
||||||
plexpy.CONFIG.FACEBOOK_TOKEN = 'temp'
|
jellypy.CONFIG.FACEBOOK_TOKEN = 'temp'
|
||||||
|
|
||||||
return facebook.auth_url(app_id=app_id,
|
return facebook.auth_url(app_id=app_id,
|
||||||
canvas_url=redirect_uri,
|
canvas_url=redirect_uri,
|
||||||
@@ -1544,9 +1544,9 @@ class FACEBOOK(Notifier):
|
|||||||
def _get_credentials(self, code=''):
|
def _get_credentials(self, code=''):
|
||||||
logger.info("Tautulli Notifiers :: Requesting access token from {name}.".format(name=self.NAME))
|
logger.info("Tautulli Notifiers :: Requesting access token from {name}.".format(name=self.NAME))
|
||||||
|
|
||||||
app_id = plexpy.CONFIG.FACEBOOK_APP_ID
|
app_id = jellypy.CONFIG.FACEBOOK_APP_ID
|
||||||
app_secret = plexpy.CONFIG.FACEBOOK_APP_SECRET
|
app_secret = jellypy.CONFIG.FACEBOOK_APP_SECRET
|
||||||
redirect_uri = plexpy.CONFIG.FACEBOOK_REDIRECT_URI
|
redirect_uri = jellypy.CONFIG.FACEBOOK_REDIRECT_URI
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Request user access token
|
# Request user access token
|
||||||
@@ -1562,17 +1562,17 @@ class FACEBOOK(Notifier):
|
|||||||
response = api.extend_access_token(app_id=app_id,
|
response = api.extend_access_token(app_id=app_id,
|
||||||
app_secret=app_secret)
|
app_secret=app_secret)
|
||||||
|
|
||||||
plexpy.CONFIG.FACEBOOK_TOKEN = response['access_token']
|
jellypy.CONFIG.FACEBOOK_TOKEN = response['access_token']
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Tautulli Notifiers :: Error requesting {name} access token: {e}".format(name=self.NAME, e=e))
|
logger.error("Tautulli Notifiers :: Error requesting {name} access token: {e}".format(name=self.NAME, e=e))
|
||||||
plexpy.CONFIG.FACEBOOK_TOKEN = ''
|
jellypy.CONFIG.FACEBOOK_TOKEN = ''
|
||||||
|
|
||||||
# Clear out temporary config values
|
# Clear out temporary config values
|
||||||
plexpy.CONFIG.FACEBOOK_APP_ID = ''
|
jellypy.CONFIG.FACEBOOK_APP_ID = ''
|
||||||
plexpy.CONFIG.FACEBOOK_APP_SECRET = ''
|
jellypy.CONFIG.FACEBOOK_APP_SECRET = ''
|
||||||
plexpy.CONFIG.FACEBOOK_REDIRECT_URI = ''
|
jellypy.CONFIG.FACEBOOK_REDIRECT_URI = ''
|
||||||
|
|
||||||
return plexpy.CONFIG.FACEBOOK_TOKEN
|
return jellypy.CONFIG.FACEBOOK_TOKEN
|
||||||
|
|
||||||
def _post_facebook(self, **data):
|
def _post_facebook(self, **data):
|
||||||
if self.config['group_id']:
|
if self.config['group_id']:
|
||||||
@@ -1820,7 +1820,7 @@ class GROWL(Notifier):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# Send it, including an image
|
# Send it, including an image
|
||||||
image_file = os.path.join(str(plexpy.PROG_DIR),
|
image_file = os.path.join(str(jellypy.PROG_DIR),
|
||||||
"data/interfaces/default/images/logo-circle.png")
|
"data/interfaces/default/images/logo-circle.png")
|
||||||
|
|
||||||
with open(image_file, 'rb') as f:
|
with open(image_file, 'rb') as f:
|
||||||
@@ -2325,7 +2325,7 @@ class PLEX(Notifier):
|
|||||||
if self.config['image']:
|
if self.config['image']:
|
||||||
image = self.config['image']
|
image = self.config['image']
|
||||||
else:
|
else:
|
||||||
image = os.path.join(plexpy.DATA_DIR, os.path.abspath("data/interfaces/default/images/logo-circle.png"))
|
image = os.path.join(jellypy.DATA_DIR, os.path.abspath("data/interfaces/default/images/logo-circle.png"))
|
||||||
|
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
logger.info("Tautulli Notifiers :: Sending notification command to {name} @ {host}".format(name=self.NAME, host=host))
|
logger.info("Tautulli Notifiers :: Sending notification command to {name} @ {host}".format(name=self.NAME, host=host))
|
||||||
@@ -2430,8 +2430,8 @@ class PLEXMOBILEAPP(Notifier):
|
|||||||
'to': self.config['user_ids'],
|
'to': self.config['user_ids'],
|
||||||
'data': {
|
'data': {
|
||||||
'provider': {
|
'provider': {
|
||||||
'identifier': plexpy.CONFIG.PMS_IDENTIFIER,
|
'identifier': jellypy.CONFIG.PMS_IDENTIFIER,
|
||||||
'title': plexpy.CONFIG.PMS_NAME
|
'title': jellypy.CONFIG.PMS_NAME
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2536,11 +2536,11 @@ class PLEXMOBILEAPP(Notifier):
|
|||||||
|
|
||||||
data['metadata'] = metadata
|
data['metadata'] = metadata
|
||||||
data['uri'] = 'server://{}/com.plexapp.plugins.library/library/metadata/{}'.format(
|
data['uri'] = 'server://{}/com.plexapp.plugins.library/library/metadata/{}'.format(
|
||||||
plexpy.CONFIG.PMS_IDENTIFIER, uri_rating_key or pretty_metadata.parameters['rating_key']
|
jellypy.CONFIG.PMS_IDENTIFIER, uri_rating_key or pretty_metadata.parameters['rating_key']
|
||||||
)
|
)
|
||||||
data['play'] = self.config['tap_action'] == 'play'
|
data['play'] = self.config['tap_action'] == 'play'
|
||||||
|
|
||||||
headers = {'X-Plex-Token': plexpy.CONFIG.PMS_TOKEN}
|
headers = {'X-Plex-Token': jellypy.CONFIG.PMS_TOKEN}
|
||||||
|
|
||||||
return self.make_request(self.NOTIFICATION_URL, headers=headers, json=data)
|
return self.make_request(self.NOTIFICATION_URL, headers=headers, json=data)
|
||||||
|
|
||||||
@@ -2977,7 +2977,7 @@ class SCRIPTS(Notifier):
|
|||||||
'.php': 'php',
|
'.php': 'php',
|
||||||
'.pl': 'perl',
|
'.pl': 'perl',
|
||||||
'.ps1': 'powershell -executionPolicy bypass -file',
|
'.ps1': 'powershell -executionPolicy bypass -file',
|
||||||
'.py': 'python' if plexpy.FROZEN else sys.executable,
|
'.py': 'python' if jellypy.FROZEN else sys.executable,
|
||||||
'.pyw': 'pythonw',
|
'.pyw': 'pythonw',
|
||||||
'.rb': 'ruby',
|
'.rb': 'ruby',
|
||||||
'.sh': ''
|
'.sh': ''
|
||||||
@@ -3013,13 +3013,13 @@ class SCRIPTS(Notifier):
|
|||||||
def run_script(self, script, user_id):
|
def run_script(self, script, user_id):
|
||||||
# Common environment variables
|
# Common environment variables
|
||||||
custom_env = {
|
custom_env = {
|
||||||
'PLEX_URL': plexpy.CONFIG.PMS_URL,
|
'PLEX_URL': jellypy.CONFIG.PMS_URL,
|
||||||
'PLEX_TOKEN': plexpy.CONFIG.PMS_TOKEN,
|
'PLEX_TOKEN': jellypy.CONFIG.PMS_TOKEN,
|
||||||
'PLEX_USER_TOKEN': '',
|
'PLEX_USER_TOKEN': '',
|
||||||
'TAUTULLI_URL': helpers.get_plexpy_url(hostname='localhost'),
|
'TAUTULLI_URL': helpers.get_plexpy_url(hostname='localhost'),
|
||||||
'TAUTULLI_PUBLIC_URL': plexpy.CONFIG.HTTP_BASE_URL + plexpy.HTTP_ROOT,
|
'TAUTULLI_PUBLIC_URL': jellypy.CONFIG.HTTP_BASE_URL + jellypy.HTTP_ROOT,
|
||||||
'TAUTULLI_APIKEY': plexpy.CONFIG.API_KEY,
|
'TAUTULLI_APIKEY': jellypy.CONFIG.API_KEY,
|
||||||
'TAUTULLI_ENCODING': plexpy.SYS_ENCODING,
|
'TAUTULLI_ENCODING': jellypy.SYS_ENCODING,
|
||||||
'TAUTULLI_PYTHON_VERSION': common.PYTHON_VERSION
|
'TAUTULLI_PYTHON_VERSION': common.PYTHON_VERSION
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3027,10 +3027,10 @@ class SCRIPTS(Notifier):
|
|||||||
user_tokens = users.Users().get_tokens(user_id=user_id)
|
user_tokens = users.Users().get_tokens(user_id=user_id)
|
||||||
custom_env['PLEX_USER_TOKEN'] = str(user_tokens['server_token'])
|
custom_env['PLEX_USER_TOKEN'] = str(user_tokens['server_token'])
|
||||||
|
|
||||||
if self.pythonpath and plexpy.INSTALL_TYPE not in ('windows', 'macos'):
|
if self.pythonpath and jellypy.INSTALL_TYPE not in ('windows', 'macos'):
|
||||||
custom_env['PYTHONPATH'] = os.pathsep.join([p for p in sys.path if p])
|
custom_env['PYTHONPATH'] = os.pathsep.join([p for p in sys.path if p])
|
||||||
|
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
custom_env = {k.encode('utf-8'): v.encode('utf-8') for k, v in custom_env.items()}
|
custom_env = {k.encode('utf-8'): v.encode('utf-8') for k, v in custom_env.items()}
|
||||||
|
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
@@ -3137,8 +3137,8 @@ class SCRIPTS(Notifier):
|
|||||||
|
|
||||||
script.extend(script_args)
|
script.extend(script_args)
|
||||||
|
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
script = [s.encode(plexpy.SYS_ENCODING, 'ignore') for s in script]
|
script = [s.encode(jellypy.SYS_ENCODING, 'ignore') for s in script]
|
||||||
|
|
||||||
logger.debug("Tautulli Notifiers :: Full script is: %s" % script)
|
logger.debug("Tautulli Notifiers :: Full script is: %s" % script)
|
||||||
logger.debug("Tautulli Notifiers :: Executing script in a new thread.")
|
logger.debug("Tautulli Notifiers :: Executing script in a new thread.")
|
||||||
@@ -3688,7 +3688,7 @@ class XBMC(Notifier):
|
|||||||
if self.config['image']:
|
if self.config['image']:
|
||||||
image = self.config['image']
|
image = self.config['image']
|
||||||
else:
|
else:
|
||||||
image = os.path.join(plexpy.DATA_DIR, os.path.abspath("data/interfaces/default/images/logo-circle.png"))
|
image = os.path.join(jellypy.DATA_DIR, os.path.abspath("data/interfaces/default/images/logo-circle.png"))
|
||||||
|
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
logger.info("Tautulli Notifiers :: Sending notification command to XMBC @ " + host)
|
logger.info("Tautulli Notifiers :: Sending notification command to XMBC @ " + host)
|
||||||
@@ -21,11 +21,11 @@ from future.builtins import str
|
|||||||
|
|
||||||
from plexapi.server import PlexServer
|
from plexapi.server import PlexServer
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
import logger
|
import logger
|
||||||
else:
|
else:
|
||||||
from plexpy import logger
|
from jellypy import logger
|
||||||
|
|
||||||
|
|
||||||
class Plex(object):
|
class Plex(object):
|
||||||
@@ -22,19 +22,19 @@ import arrow
|
|||||||
import sqlite3
|
import sqlite3
|
||||||
from xml.dom import minidom
|
from xml.dom import minidom
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
import activity_processor
|
import activity_processor
|
||||||
import database
|
import database
|
||||||
import helpers
|
import helpers
|
||||||
import logger
|
import logger
|
||||||
import users
|
import users
|
||||||
else:
|
else:
|
||||||
from plexpy import activity_processor
|
from jellypy import activity_processor
|
||||||
from plexpy import database
|
from jellypy import database
|
||||||
from plexpy import helpers
|
from jellypy import helpers
|
||||||
from plexpy import logger
|
from jellypy import logger
|
||||||
from plexpy import users
|
from jellypy import users
|
||||||
|
|
||||||
|
|
||||||
def extract_plexivity_xml(xml=None):
|
def extract_plexivity_xml(xml=None):
|
||||||
@@ -23,8 +23,8 @@ from future.builtins import object
|
|||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
import common
|
import common
|
||||||
import helpers
|
import helpers
|
||||||
import http_handler
|
import http_handler
|
||||||
@@ -33,31 +33,31 @@ if plexpy.PYTHON2:
|
|||||||
import pmsconnect
|
import pmsconnect
|
||||||
import session
|
import session
|
||||||
else:
|
else:
|
||||||
from plexpy import common
|
from jellypy import common
|
||||||
from plexpy import helpers
|
from jellypy import helpers
|
||||||
from plexpy import http_handler
|
from jellypy import http_handler
|
||||||
from plexpy import logger
|
from jellypy import logger
|
||||||
from plexpy import users
|
from jellypy import users
|
||||||
from plexpy import pmsconnect
|
from jellypy import pmsconnect
|
||||||
from plexpy import session
|
from jellypy import session
|
||||||
|
|
||||||
|
|
||||||
def get_server_resources(return_presence=False, return_server=False, return_info=False, **kwargs):
|
def get_server_resources(return_presence=False, return_server=False, return_info=False, **kwargs):
|
||||||
if not return_presence and not return_info:
|
if not return_presence and not return_info:
|
||||||
logger.info("Tautulli PlexTV :: Requesting resources for server...")
|
logger.info("Tautulli PlexTV :: Requesting resources for server...")
|
||||||
|
|
||||||
server = {'pms_name': plexpy.CONFIG.PMS_NAME,
|
server = {'pms_name': jellypy.CONFIG.PMS_NAME,
|
||||||
'pms_version': plexpy.CONFIG.PMS_VERSION,
|
'pms_version': jellypy.CONFIG.PMS_VERSION,
|
||||||
'pms_platform': plexpy.CONFIG.PMS_PLATFORM,
|
'pms_platform': jellypy.CONFIG.PMS_PLATFORM,
|
||||||
'pms_ip': plexpy.CONFIG.PMS_IP,
|
'pms_ip': jellypy.CONFIG.PMS_IP,
|
||||||
'pms_port': plexpy.CONFIG.PMS_PORT,
|
'pms_port': jellypy.CONFIG.PMS_PORT,
|
||||||
'pms_ssl': plexpy.CONFIG.PMS_SSL,
|
'pms_ssl': jellypy.CONFIG.PMS_SSL,
|
||||||
'pms_is_remote': plexpy.CONFIG.PMS_IS_REMOTE,
|
'pms_is_remote': jellypy.CONFIG.PMS_IS_REMOTE,
|
||||||
'pms_is_cloud': plexpy.CONFIG.PMS_IS_CLOUD,
|
'pms_is_cloud': jellypy.CONFIG.PMS_IS_CLOUD,
|
||||||
'pms_url': plexpy.CONFIG.PMS_URL,
|
'pms_url': jellypy.CONFIG.PMS_URL,
|
||||||
'pms_url_manual': plexpy.CONFIG.PMS_URL_MANUAL,
|
'pms_url_manual': jellypy.CONFIG.PMS_URL_MANUAL,
|
||||||
'pms_identifier': plexpy.CONFIG.PMS_IDENTIFIER,
|
'pms_identifier': jellypy.CONFIG.PMS_IDENTIFIER,
|
||||||
'pms_plexpass': plexpy.CONFIG.PMS_PLEXPASS
|
'pms_plexpass': jellypy.CONFIG.PMS_PLEXPASS
|
||||||
}
|
}
|
||||||
|
|
||||||
if return_info:
|
if return_info:
|
||||||
@@ -132,8 +132,8 @@ def get_server_resources(return_presence=False, return_server=False, return_info
|
|||||||
logger.info("Tautulli PlexTV :: Selected server: %s (%s) (%s - Version %s)",
|
logger.info("Tautulli PlexTV :: Selected server: %s (%s) (%s - Version %s)",
|
||||||
server['pms_name'], server['pms_url'], server['pms_platform'], server['pms_version'])
|
server['pms_name'], server['pms_url'], server['pms_platform'], server['pms_version'])
|
||||||
|
|
||||||
plexpy.CONFIG.process_kwargs(server)
|
jellypy.CONFIG.process_kwargs(server)
|
||||||
plexpy.CONFIG.write()
|
jellypy.CONFIG.write()
|
||||||
|
|
||||||
|
|
||||||
class PlexTV(object):
|
class PlexTV(object):
|
||||||
@@ -147,8 +147,8 @@ class PlexTV(object):
|
|||||||
self.token = token
|
self.token = token
|
||||||
|
|
||||||
self.urls = 'https://plex.tv'
|
self.urls = 'https://plex.tv'
|
||||||
self.timeout = plexpy.CONFIG.PMS_TIMEOUT
|
self.timeout = jellypy.CONFIG.PMS_TIMEOUT
|
||||||
self.ssl_verify = plexpy.CONFIG.VERIFY_SSL_CERT
|
self.ssl_verify = jellypy.CONFIG.VERIFY_SSL_CERT
|
||||||
|
|
||||||
if self.username is None and self.password is None:
|
if self.username is None and self.password is None:
|
||||||
if not self.token:
|
if not self.token:
|
||||||
@@ -158,7 +158,7 @@ class PlexTV(object):
|
|||||||
user_tokens = user_data.get_tokens(user_id=session.get_session_user_id())
|
user_tokens = user_data.get_tokens(user_id=session.get_session_user_id())
|
||||||
self.token = user_tokens['server_token']
|
self.token = user_tokens['server_token']
|
||||||
else:
|
else:
|
||||||
self.token = plexpy.CONFIG.PMS_TOKEN
|
self.token = jellypy.CONFIG.PMS_TOKEN
|
||||||
|
|
||||||
if not self.token:
|
if not self.token:
|
||||||
logger.error("Tautulli PlexTV :: PlexTV called, but no token provided.")
|
logger.error("Tautulli PlexTV :: PlexTV called, but no token provided.")
|
||||||
@@ -212,7 +212,7 @@ class PlexTV(object):
|
|||||||
if force:
|
if force:
|
||||||
logger.debug("Tautulli PlexTV :: Forcing refresh of Plex.tv token.")
|
logger.debug("Tautulli PlexTV :: Forcing refresh of Plex.tv token.")
|
||||||
devices_list = self.get_devices_list()
|
devices_list = self.get_devices_list()
|
||||||
device_id = next((d for d in devices_list if d['device_identifier'] == plexpy.CONFIG.PMS_UUID), {}).get('device_id', None)
|
device_id = next((d for d in devices_list if d['device_identifier'] == jellypy.CONFIG.PMS_UUID), {}).get('device_id', None)
|
||||||
|
|
||||||
if device_id:
|
if device_id:
|
||||||
logger.debug("Tautulli PlexTV :: Removing Tautulli from Plex.tv devices.")
|
logger.debug("Tautulli PlexTV :: Removing Tautulli from Plex.tv devices.")
|
||||||
@@ -228,8 +228,8 @@ class PlexTV(object):
|
|||||||
user = self.get_token()
|
user = self.get_token()
|
||||||
if user:
|
if user:
|
||||||
token = user['auth_token']
|
token = user['auth_token']
|
||||||
plexpy.CONFIG.__setattr__('PMS_TOKEN', token)
|
jellypy.CONFIG.__setattr__('PMS_TOKEN', token)
|
||||||
plexpy.CONFIG.write()
|
jellypy.CONFIG.write()
|
||||||
logger.info("Tautulli PlexTV :: Updated Plex.tv token for Tautulli.")
|
logger.info("Tautulli PlexTV :: Updated Plex.tv token for Tautulli.")
|
||||||
return token
|
return token
|
||||||
|
|
||||||
@@ -245,7 +245,7 @@ class PlexTV(object):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
for a in xml_head:
|
for a in xml_head:
|
||||||
if helpers.get_xml_attr(a, 'clientIdentifier') == plexpy.CONFIG.PMS_IDENTIFIER \
|
if helpers.get_xml_attr(a, 'clientIdentifier') == jellypy.CONFIG.PMS_IDENTIFIER \
|
||||||
and 'server' in helpers.get_xml_attr(a, 'provides'):
|
and 'server' in helpers.get_xml_attr(a, 'provides'):
|
||||||
server_token = helpers.get_xml_attr(a, 'accessToken')
|
server_token = helpers.get_xml_attr(a, 'accessToken')
|
||||||
break
|
break
|
||||||
@@ -412,7 +412,7 @@ class PlexTV(object):
|
|||||||
def get_full_users_list(self):
|
def get_full_users_list(self):
|
||||||
own_account = self.get_plextv_user_details(output_format='xml')
|
own_account = self.get_plextv_user_details(output_format='xml')
|
||||||
friends_list = self.get_plextv_friends(output_format='xml')
|
friends_list = self.get_plextv_friends(output_format='xml')
|
||||||
shared_servers = self.get_plextv_shared_servers(machine_id=plexpy.CONFIG.PMS_IDENTIFIER,
|
shared_servers = self.get_plextv_shared_servers(machine_id=jellypy.CONFIG.PMS_IDENTIFIER,
|
||||||
output_format='xml')
|
output_format='xml')
|
||||||
|
|
||||||
users_list = []
|
users_list = []
|
||||||
@@ -498,7 +498,7 @@ class PlexTV(object):
|
|||||||
rating_key_filter=None, sync_id_filter=None):
|
rating_key_filter=None, sync_id_filter=None):
|
||||||
|
|
||||||
if not machine_id:
|
if not machine_id:
|
||||||
machine_id = plexpy.CONFIG.PMS_IDENTIFIER
|
machine_id = jellypy.CONFIG.PMS_IDENTIFIER
|
||||||
|
|
||||||
if isinstance(rating_key_filter, list):
|
if isinstance(rating_key_filter, list):
|
||||||
rating_key_filter = [str(k) for k in rating_key_filter]
|
rating_key_filter = [str(k) for k in rating_key_filter]
|
||||||
@@ -709,7 +709,7 @@ class PlexTV(object):
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
for a in xml_head:
|
for a in xml_head:
|
||||||
if helpers.get_xml_attr(a, 'machineIdentifier') == plexpy.CONFIG.PMS_IDENTIFIER:
|
if helpers.get_xml_attr(a, 'machineIdentifier') == jellypy.CONFIG.PMS_IDENTIFIER:
|
||||||
server_times = {"created_at": helpers.get_xml_attr(a, 'createdAt'),
|
server_times = {"created_at": helpers.get_xml_attr(a, 'createdAt'),
|
||||||
"updated_at": helpers.get_xml_attr(a, 'updatedAt'),
|
"updated_at": helpers.get_xml_attr(a, 'updatedAt'),
|
||||||
"version": helpers.get_xml_attr(a, 'version')
|
"version": helpers.get_xml_attr(a, 'version')
|
||||||
@@ -824,7 +824,7 @@ class PlexTV(object):
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
# Get the updates for the platform
|
# Get the updates for the platform
|
||||||
pms_platform = common.PMS_PLATFORM_NAME_OVERRIDES.get(plexpy.CONFIG.PMS_PLATFORM, plexpy.CONFIG.PMS_PLATFORM)
|
pms_platform = common.PMS_PLATFORM_NAME_OVERRIDES.get(jellypy.CONFIG.PMS_PLATFORM, jellypy.CONFIG.PMS_PLATFORM)
|
||||||
platform_downloads = available_downloads.get('computer').get(pms_platform) or \
|
platform_downloads = available_downloads.get('computer').get(pms_platform) or \
|
||||||
available_downloads.get('nas').get(pms_platform)
|
available_downloads.get('nas').get(pms_platform)
|
||||||
|
|
||||||
@@ -833,12 +833,12 @@ class PlexTV(object):
|
|||||||
% pms_platform)
|
% pms_platform)
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
v_old = helpers.cast_to_int("".join(v.zfill(4) for v in plexpy.CONFIG.PMS_VERSION.split('-')[0].split('.')[:4]))
|
v_old = helpers.cast_to_int("".join(v.zfill(4) for v in jellypy.CONFIG.PMS_VERSION.split('-')[0].split('.')[:4]))
|
||||||
v_new = helpers.cast_to_int("".join(v.zfill(4) for v in platform_downloads.get('version', '').split('-')[0].split('.')[:4]))
|
v_new = helpers.cast_to_int("".join(v.zfill(4) for v in platform_downloads.get('version', '').split('-')[0].split('.')[:4]))
|
||||||
|
|
||||||
if not v_old:
|
if not v_old:
|
||||||
logger.error("Tautulli PlexTV :: Unable to retrieve Plex updates: Invalid current server version: %s."
|
logger.error("Tautulli PlexTV :: Unable to retrieve Plex updates: Invalid current server version: %s."
|
||||||
% plexpy.CONFIG.PMS_VERSION)
|
% jellypy.CONFIG.PMS_VERSION)
|
||||||
return {}
|
return {}
|
||||||
if not v_new:
|
if not v_new:
|
||||||
logger.error("Tautulli PlexTV :: Unable to retrieve Plex updates: Invalid new server version: %s."
|
logger.error("Tautulli PlexTV :: Unable to retrieve Plex updates: Invalid new server version: %s."
|
||||||
@@ -847,8 +847,8 @@ class PlexTV(object):
|
|||||||
|
|
||||||
# Get proper download
|
# Get proper download
|
||||||
releases = platform_downloads.get('releases', [{}])
|
releases = platform_downloads.get('releases', [{}])
|
||||||
release = next((r for r in releases if r['distro'] == plexpy.CONFIG.PMS_UPDATE_DISTRO and
|
release = next((r for r in releases if r['distro'] == jellypy.CONFIG.PMS_UPDATE_DISTRO and
|
||||||
r['build'] == plexpy.CONFIG.PMS_UPDATE_DISTRO_BUILD), releases[0])
|
r['build'] == jellypy.CONFIG.PMS_UPDATE_DISTRO_BUILD), releases[0])
|
||||||
|
|
||||||
download_info = {'update_available': v_new > v_old,
|
download_info = {'update_available': v_new > v_old,
|
||||||
'platform': platform_downloads.get('name'),
|
'platform': platform_downloads.get('name'),
|
||||||
@@ -876,13 +876,13 @@ class PlexTV(object):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
if subscription and helpers.get_xml_attr(subscription[0], 'active') == '1':
|
if subscription and helpers.get_xml_attr(subscription[0], 'active') == '1':
|
||||||
plexpy.CONFIG.__setattr__('PMS_PLEXPASS', 1)
|
jellypy.CONFIG.__setattr__('PMS_PLEXPASS', 1)
|
||||||
plexpy.CONFIG.write()
|
jellypy.CONFIG.write()
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
logger.debug("Tautulli PlexTV :: Plex Pass subscription not found.")
|
logger.debug("Tautulli PlexTV :: Plex Pass subscription not found.")
|
||||||
plexpy.CONFIG.__setattr__('PMS_PLEXPASS', 0)
|
jellypy.CONFIG.__setattr__('PMS_PLEXPASS', 0)
|
||||||
plexpy.CONFIG.write()
|
jellypy.CONFIG.write()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_devices_list(self):
|
def get_devices_list(self):
|
||||||
@@ -925,7 +925,7 @@ class PlexTV(object):
|
|||||||
for info in status_info:
|
for info in status_info:
|
||||||
servers = info.getElementsByTagName('server')
|
servers = info.getElementsByTagName('server')
|
||||||
for s in servers:
|
for s in servers:
|
||||||
if helpers.get_xml_attr(s, 'address') == plexpy.CONFIG.PMS_IP:
|
if helpers.get_xml_attr(s, 'address') == jellypy.CONFIG.PMS_IP:
|
||||||
if helpers.get_xml_attr(info, 'running') == '1':
|
if helpers.get_xml_attr(info, 'running') == '1':
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
@@ -21,19 +21,19 @@ from future.builtins import str
|
|||||||
import sqlite3
|
import sqlite3
|
||||||
from xml.dom import minidom
|
from xml.dom import minidom
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
import activity_processor
|
import activity_processor
|
||||||
import database
|
import database
|
||||||
import helpers
|
import helpers
|
||||||
import logger
|
import logger
|
||||||
import users
|
import users
|
||||||
else:
|
else:
|
||||||
from plexpy import activity_processor
|
from jellypy import activity_processor
|
||||||
from plexpy import database
|
from jellypy import database
|
||||||
from plexpy import helpers
|
from jellypy import helpers
|
||||||
from plexpy import logger
|
from jellypy import logger
|
||||||
from plexpy import users
|
from jellypy import users
|
||||||
|
|
||||||
|
|
||||||
def extract_plexwatch_xml(xml=None):
|
def extract_plexwatch_xml(xml=None):
|
||||||
@@ -26,8 +26,8 @@ import time
|
|||||||
from future.moves.urllib.parse import quote, quote_plus, urlencode
|
from future.moves.urllib.parse import quote, quote_plus, urlencode
|
||||||
from xml.dom.minidom import Node
|
from xml.dom.minidom import Node
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
import activity_processor
|
import activity_processor
|
||||||
import common
|
import common
|
||||||
import helpers
|
import helpers
|
||||||
@@ -38,15 +38,15 @@ if plexpy.PYTHON2:
|
|||||||
import session
|
import session
|
||||||
import users
|
import users
|
||||||
else:
|
else:
|
||||||
from plexpy import activity_processor
|
from jellypy import activity_processor
|
||||||
from plexpy import common
|
from jellypy import common
|
||||||
from plexpy import helpers
|
from jellypy import helpers
|
||||||
from plexpy import http_handler
|
from jellypy import http_handler
|
||||||
from plexpy import libraries
|
from jellypy import libraries
|
||||||
from plexpy import logger
|
from jellypy import logger
|
||||||
from plexpy import plextv
|
from jellypy import plextv
|
||||||
from plexpy import session
|
from jellypy import session
|
||||||
from plexpy import users
|
from jellypy import users
|
||||||
|
|
||||||
|
|
||||||
def get_server_friendly_name():
|
def get_server_friendly_name():
|
||||||
@@ -57,13 +57,13 @@ def get_server_friendly_name():
|
|||||||
if not server_name:
|
if not server_name:
|
||||||
servers_info = PmsConnect().get_servers_info()
|
servers_info = PmsConnect().get_servers_info()
|
||||||
for server in servers_info:
|
for server in servers_info:
|
||||||
if server['machine_identifier'] == plexpy.CONFIG.PMS_IDENTIFIER:
|
if server['machine_identifier'] == jellypy.CONFIG.PMS_IDENTIFIER:
|
||||||
server_name = server['name']
|
server_name = server['name']
|
||||||
break
|
break
|
||||||
|
|
||||||
if server_name and server_name != plexpy.CONFIG.PMS_NAME:
|
if server_name and server_name != jellypy.CONFIG.PMS_NAME:
|
||||||
plexpy.CONFIG.__setattr__('PMS_NAME', server_name)
|
jellypy.CONFIG.__setattr__('PMS_NAME', server_name)
|
||||||
plexpy.CONFIG.write()
|
jellypy.CONFIG.write()
|
||||||
logger.info("Tautulli Pmsconnect :: Server name retrieved.")
|
logger.info("Tautulli Pmsconnect :: Server name retrieved.")
|
||||||
|
|
||||||
return server_name
|
return server_name
|
||||||
@@ -78,12 +78,12 @@ class PmsConnect(object):
|
|||||||
self.url = url
|
self.url = url
|
||||||
self.token = token
|
self.token = token
|
||||||
|
|
||||||
if not self.url and plexpy.CONFIG.PMS_URL:
|
if not self.url and jellypy.CONFIG.PMS_URL:
|
||||||
self.url = plexpy.CONFIG.PMS_URL
|
self.url = jellypy.CONFIG.PMS_URL
|
||||||
elif not self.url:
|
elif not self.url:
|
||||||
self.url = 'http://{hostname}:{port}'.format(hostname=plexpy.CONFIG.PMS_IP,
|
self.url = 'http://{hostname}:{port}'.format(hostname=jellypy.CONFIG.PMS_IP,
|
||||||
port=plexpy.CONFIG.PMS_PORT)
|
port=jellypy.CONFIG.PMS_PORT)
|
||||||
self.timeout = plexpy.CONFIG.PMS_TIMEOUT
|
self.timeout = jellypy.CONFIG.PMS_TIMEOUT
|
||||||
|
|
||||||
if not self.token:
|
if not self.token:
|
||||||
# Check if we should use the admin token, or the guest server token
|
# Check if we should use the admin token, or the guest server token
|
||||||
@@ -92,7 +92,7 @@ class PmsConnect(object):
|
|||||||
user_tokens = user_data.get_tokens(user_id=session.get_session_user_id())
|
user_tokens = user_data.get_tokens(user_id=session.get_session_user_id())
|
||||||
self.token = user_tokens['server_token']
|
self.token = user_tokens['server_token']
|
||||||
else:
|
else:
|
||||||
self.token = plexpy.CONFIG.PMS_TOKEN
|
self.token = jellypy.CONFIG.PMS_TOKEN
|
||||||
|
|
||||||
self.request_handler = http_handler.HTTPHandler(urls=self.url,
|
self.request_handler = http_handler.HTTPHandler(urls=self.url,
|
||||||
token=self.token,
|
token=self.token,
|
||||||
@@ -625,7 +625,7 @@ class PmsConnect(object):
|
|||||||
metadata = {}
|
metadata = {}
|
||||||
|
|
||||||
if not skip_cache and cache_key:
|
if not skip_cache and cache_key:
|
||||||
in_file_folder = os.path.join(plexpy.CONFIG.CACHE_DIR, 'session_metadata')
|
in_file_folder = os.path.join(jellypy.CONFIG.CACHE_DIR, 'session_metadata')
|
||||||
in_file_path = os.path.join(in_file_folder, 'metadata-sessionKey-%s.json' % cache_key)
|
in_file_path = os.path.join(in_file_folder, 'metadata-sessionKey-%s.json' % cache_key)
|
||||||
|
|
||||||
if not os.path.exists(in_file_folder):
|
if not os.path.exists(in_file_folder):
|
||||||
@@ -640,7 +640,7 @@ class PmsConnect(object):
|
|||||||
if metadata:
|
if metadata:
|
||||||
_cache_time = metadata.pop('_cache_time', 0)
|
_cache_time = metadata.pop('_cache_time', 0)
|
||||||
# Return cached metadata if less than cache_seconds ago
|
# Return cached metadata if less than cache_seconds ago
|
||||||
if return_cache or helpers.timestamp() - _cache_time <= plexpy.CONFIG.METADATA_CACHE_SECONDS:
|
if return_cache or helpers.timestamp() - _cache_time <= jellypy.CONFIG.METADATA_CACHE_SECONDS:
|
||||||
return metadata
|
return metadata
|
||||||
|
|
||||||
if rating_key:
|
if rating_key:
|
||||||
@@ -649,7 +649,7 @@ class PmsConnect(object):
|
|||||||
metadata_xml = self.get_sync_item(str(sync_id), output_format='xml')
|
metadata_xml = self.get_sync_item(str(sync_id), output_format='xml')
|
||||||
elif plex_guid.startswith(('plex://movie', 'plex://episode')):
|
elif plex_guid.startswith(('plex://movie', 'plex://episode')):
|
||||||
rating_key = plex_guid.rsplit('/', 1)[-1]
|
rating_key = plex_guid.rsplit('/', 1)[-1]
|
||||||
plextv_metadata = PmsConnect(url='https://metadata.provider.plex.tv', token=plexpy.CONFIG.PMS_TOKEN)
|
plextv_metadata = PmsConnect(url='https://metadata.provider.plex.tv', token=jellypy.CONFIG.PMS_TOKEN)
|
||||||
metadata_xml = plextv_metadata.get_metadata(rating_key, output_format='xml')
|
metadata_xml = plextv_metadata.get_metadata(rating_key, output_format='xml')
|
||||||
else:
|
else:
|
||||||
return metadata
|
return metadata
|
||||||
@@ -1474,7 +1474,7 @@ class PmsConnect(object):
|
|||||||
if cache_key:
|
if cache_key:
|
||||||
metadata['_cache_time'] = helpers.timestamp()
|
metadata['_cache_time'] = helpers.timestamp()
|
||||||
|
|
||||||
out_file_folder = os.path.join(plexpy.CONFIG.CACHE_DIR, 'session_metadata')
|
out_file_folder = os.path.join(jellypy.CONFIG.CACHE_DIR, 'session_metadata')
|
||||||
out_file_path = os.path.join(out_file_folder, 'metadata-sessionKey-%s.json' % cache_key)
|
out_file_path = os.path.join(out_file_folder, 'metadata-sessionKey-%s.json' % cache_key)
|
||||||
|
|
||||||
if not os.path.exists(out_file_folder):
|
if not os.path.exists(out_file_folder):
|
||||||
@@ -1782,7 +1782,7 @@ class PmsConnect(object):
|
|||||||
and not session.getElementsByTagName('Session') \
|
and not session.getElementsByTagName('Session') \
|
||||||
and not session.getElementsByTagName('TranscodeSession') \
|
and not session.getElementsByTagName('TranscodeSession') \
|
||||||
and helpers.get_xml_attr(session, 'ratingKey').isdigit() \
|
and helpers.get_xml_attr(session, 'ratingKey').isdigit() \
|
||||||
and plexpy.CONFIG.PMS_PLEXPASS:
|
and jellypy.CONFIG.PMS_PLEXPASS:
|
||||||
plex_tv = plextv.PlexTV()
|
plex_tv = plextv.PlexTV()
|
||||||
parent_rating_key = helpers.get_xml_attr(session, 'parentRatingKey')
|
parent_rating_key = helpers.get_xml_attr(session, 'parentRatingKey')
|
||||||
grandparent_rating_key = helpers.get_xml_attr(session, 'grandparentRatingKey')
|
grandparent_rating_key = helpers.get_xml_attr(session, 'grandparentRatingKey')
|
||||||
@@ -3101,9 +3101,9 @@ class PmsConnect(object):
|
|||||||
logger.info("Tautulli is unable to check for Plex updates. Disabling check for Plex updates.")
|
logger.info("Tautulli is unable to check for Plex updates. Disabling check for Plex updates.")
|
||||||
|
|
||||||
# Disable check for Plex updates
|
# Disable check for Plex updates
|
||||||
plexpy.CONFIG.MONITOR_PMS_UPDATES = 0
|
jellypy.CONFIG.MONITOR_PMS_UPDATES = 0
|
||||||
plexpy.initialize_scheduler()
|
jellypy.initialize_scheduler()
|
||||||
plexpy.CONFIG.write()
|
jellypy.CONFIG.write()
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@@ -3123,13 +3123,13 @@ class PmsConnect(object):
|
|||||||
|
|
||||||
def set_server_version(self):
|
def set_server_version(self):
|
||||||
identity = self.get_server_identity()
|
identity = self.get_server_identity()
|
||||||
version = identity.get('version', plexpy.CONFIG.PMS_VERSION)
|
version = identity.get('version', jellypy.CONFIG.PMS_VERSION)
|
||||||
|
|
||||||
plexpy.CONFIG.__setattr__('PMS_VERSION', version)
|
jellypy.CONFIG.__setattr__('PMS_VERSION', version)
|
||||||
plexpy.CONFIG.write()
|
jellypy.CONFIG.write()
|
||||||
|
|
||||||
def get_server_update_channel(self):
|
def get_server_update_channel(self):
|
||||||
if plexpy.CONFIG.PMS_UPDATE_CHANNEL == 'plex':
|
if jellypy.CONFIG.PMS_UPDATE_CHANNEL == 'plex':
|
||||||
update_channel_value = self.get_server_pref('ButlerUpdateChannel')
|
update_channel_value = self.get_server_pref('ButlerUpdateChannel')
|
||||||
|
|
||||||
if update_channel_value == '8':
|
if update_channel_value == '8':
|
||||||
@@ -3137,4 +3137,4 @@ class PmsConnect(object):
|
|||||||
else:
|
else:
|
||||||
return 'public'
|
return 'public'
|
||||||
|
|
||||||
return plexpy.CONFIG.PMS_UPDATE_CHANNEL
|
return jellypy.CONFIG.PMS_UPDATE_CHANNEL
|
||||||
@@ -25,13 +25,13 @@ import collections
|
|||||||
import requests
|
import requests
|
||||||
from requests.packages import urllib3
|
from requests.packages import urllib3
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
import lock
|
import lock
|
||||||
import logger
|
import logger
|
||||||
else:
|
else:
|
||||||
from plexpy import lock
|
from jellypy import lock
|
||||||
from plexpy import logger
|
from jellypy import logger
|
||||||
|
|
||||||
|
|
||||||
# Dictionary with last request times, for rate limiting.
|
# Dictionary with last request times, for rate limiting.
|
||||||
@@ -59,7 +59,7 @@ def request_response(url, method="get", auto_raise=True,
|
|||||||
|
|
||||||
# Disable verification of SSL certificates if requested. Note: this could
|
# Disable verification of SSL certificates if requested. Note: this could
|
||||||
# pose a security issue!
|
# pose a security issue!
|
||||||
kwargs["verify"] = bool(plexpy.CONFIG.VERIFY_SSL_CERT)
|
kwargs["verify"] = bool(jellypy.CONFIG.VERIFY_SSL_CERT)
|
||||||
if not kwargs['verify']:
|
if not kwargs['verify']:
|
||||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ def request_response(url, method="get", auto_raise=True,
|
|||||||
e.response.status_code, cause)
|
e.response.status_code, cause)
|
||||||
|
|
||||||
# Debug response
|
# Debug response
|
||||||
if plexpy.VERBOSE:
|
if jellypy.VERBOSE:
|
||||||
server_message(e.response)
|
server_message(e.response)
|
||||||
else:
|
else:
|
||||||
logger.error("Request raised HTTP error.")
|
logger.error("Request raised HTTP error.")
|
||||||
@@ -151,7 +151,7 @@ def request_response2(url, method="get", auto_raise=True,
|
|||||||
|
|
||||||
# Disable verification of SSL certificates if requested. Note: this could
|
# Disable verification of SSL certificates if requested. Note: this could
|
||||||
# pose a security issue!
|
# pose a security issue!
|
||||||
kwargs['verify'] = bool(plexpy.CONFIG.VERIFY_SSL_CERT)
|
kwargs['verify'] = bool(jellypy.CONFIG.VERIFY_SSL_CERT)
|
||||||
if not kwargs['verify']:
|
if not kwargs['verify']:
|
||||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
|
|
||||||
@@ -203,7 +203,7 @@ def request_response2(url, method="get", auto_raise=True,
|
|||||||
|
|
||||||
err_msg = "Request raised a HTTP error: {}".format(http_err)
|
err_msg = "Request raised a HTTP error: {}".format(http_err)
|
||||||
|
|
||||||
if plexpy.VERBOSE:
|
if jellypy.VERBOSE:
|
||||||
req_msg = server_message(e.response, return_msg=True)
|
req_msg = server_message(e.response, return_msg=True)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -264,7 +264,7 @@ def request_json(url, **kwargs):
|
|||||||
logger.error("Response returned invalid JSON data")
|
logger.error("Response returned invalid JSON data")
|
||||||
|
|
||||||
# Debug response
|
# Debug response
|
||||||
if plexpy.VERBOSE:
|
if jellypy.VERBOSE:
|
||||||
server_message(response)
|
server_message(response)
|
||||||
|
|
||||||
|
|
||||||
@@ -20,13 +20,13 @@ from future.builtins import str
|
|||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
import common
|
import common
|
||||||
import users
|
import users
|
||||||
else:
|
else:
|
||||||
from plexpy import common
|
from jellypy import common
|
||||||
from plexpy import users
|
from jellypy import users
|
||||||
|
|
||||||
|
|
||||||
def get_session_info():
|
def get_session_info():
|
||||||
@@ -68,7 +68,7 @@ def get_session_user_token():
|
|||||||
session_user_tokens = users.Users().get_tokens(_session['user_id'])
|
session_user_tokens = users.Users().get_tokens(_session['user_id'])
|
||||||
user_token = session_user_tokens['server_token']
|
user_token = session_user_tokens['server_token']
|
||||||
else:
|
else:
|
||||||
user_token = plexpy.CONFIG.PMS_TOKEN
|
user_token = jellypy.CONFIG.PMS_TOKEN
|
||||||
|
|
||||||
return user_token
|
return user_token
|
||||||
|
|
||||||
@@ -23,8 +23,8 @@ from future.moves.urllib.parse import parse_qsl
|
|||||||
|
|
||||||
import httpagentparser
|
import httpagentparser
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
import common
|
import common
|
||||||
import database
|
import database
|
||||||
import datatables
|
import datatables
|
||||||
@@ -34,21 +34,21 @@ if plexpy.PYTHON2:
|
|||||||
import plextv
|
import plextv
|
||||||
import session
|
import session
|
||||||
else:
|
else:
|
||||||
from plexpy import common
|
from jellypy import common
|
||||||
from plexpy import database
|
from jellypy import database
|
||||||
from plexpy import datatables
|
from jellypy import datatables
|
||||||
from plexpy import helpers
|
from jellypy import helpers
|
||||||
from plexpy import libraries
|
from jellypy import libraries
|
||||||
from plexpy import logger
|
from jellypy import logger
|
||||||
from plexpy import plextv
|
from jellypy import plextv
|
||||||
from plexpy import session
|
from jellypy import session
|
||||||
|
|
||||||
|
|
||||||
def refresh_users():
|
def refresh_users():
|
||||||
logger.info("Tautulli Users :: Requesting users list refresh...")
|
logger.info("Tautulli Users :: Requesting users list refresh...")
|
||||||
result = plextv.PlexTV().get_full_users_list()
|
result = plextv.PlexTV().get_full_users_list()
|
||||||
|
|
||||||
server_id = plexpy.CONFIG.PMS_IDENTIFIER
|
server_id = jellypy.CONFIG.PMS_IDENTIFIER
|
||||||
if not server_id:
|
if not server_id:
|
||||||
logger.error("Tautulli Users :: No PMS identifier, cannot refresh users. Verify server in settings.")
|
logger.error("Tautulli Users :: No PMS identifier, cannot refresh users. Verify server in settings.")
|
||||||
return
|
return
|
||||||
@@ -111,7 +111,7 @@ class Users(object):
|
|||||||
custom_where = [['users.deleted_user', 0]]
|
custom_where = [['users.deleted_user', 0]]
|
||||||
|
|
||||||
if grouping is None:
|
if grouping is None:
|
||||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
grouping = jellypy.CONFIG.GROUP_HISTORY_TABLES
|
||||||
|
|
||||||
if session.get_session_user_id():
|
if session.get_session_user_id():
|
||||||
custom_where.append(['users.user_id', session.get_session_user_id()])
|
custom_where.append(['users.user_id', session.get_session_user_id()])
|
||||||
@@ -486,7 +486,7 @@ class Users(object):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
if grouping is None:
|
if grouping is None:
|
||||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
grouping = jellypy.CONFIG.GROUP_HISTORY_TABLES
|
||||||
|
|
||||||
if query_days and query_days is not None:
|
if query_days and query_days is not None:
|
||||||
query_days = map(helpers.cast_to_int, query_days.split(','))
|
query_days = map(helpers.cast_to_int, query_days.split(','))
|
||||||
@@ -548,7 +548,7 @@ class Users(object):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
if grouping is None:
|
if grouping is None:
|
||||||
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
|
grouping = jellypy.CONFIG.GROUP_HISTORY_TABLES
|
||||||
|
|
||||||
monitor_db = database.MonitorDatabase()
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
@@ -27,23 +27,23 @@ import re
|
|||||||
import subprocess
|
import subprocess
|
||||||
import tarfile
|
import tarfile
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
import common
|
import common
|
||||||
import helpers
|
import helpers
|
||||||
import logger
|
import logger
|
||||||
import request
|
import request
|
||||||
else:
|
else:
|
||||||
from plexpy import common
|
from jellypy import common
|
||||||
from plexpy import helpers
|
from jellypy import helpers
|
||||||
from plexpy import logger
|
from jellypy import logger
|
||||||
from plexpy import request
|
from jellypy import request
|
||||||
|
|
||||||
|
|
||||||
def runGit(args):
|
def runGit(args):
|
||||||
|
|
||||||
if plexpy.CONFIG.GIT_PATH:
|
if jellypy.CONFIG.GIT_PATH:
|
||||||
git_locations = ['"' + plexpy.CONFIG.GIT_PATH + '"']
|
git_locations = ['"' + jellypy.CONFIG.GIT_PATH + '"']
|
||||||
else:
|
else:
|
||||||
git_locations = ['git']
|
git_locations = ['git']
|
||||||
|
|
||||||
@@ -56,8 +56,8 @@ def runGit(args):
|
|||||||
cmd = cur_git + ' ' + args
|
cmd = cur_git + ' ' + args
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logger.debug('Trying to execute: "' + cmd + '" with shell in ' + plexpy.PROG_DIR)
|
logger.debug('Trying to execute: "' + cmd + '" with shell in ' + jellypy.PROG_DIR)
|
||||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd=plexpy.PROG_DIR)
|
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd=jellypy.PROG_DIR)
|
||||||
output, err = p.communicate()
|
output, err = p.communicate()
|
||||||
output = output.strip().decode()
|
output = output.strip().decode()
|
||||||
|
|
||||||
@@ -80,18 +80,18 @@ def runGit(args):
|
|||||||
|
|
||||||
def get_version():
|
def get_version():
|
||||||
|
|
||||||
if plexpy.FROZEN and common.PLATFORM == 'Windows':
|
if jellypy.FROZEN and common.PLATFORM == 'Windows':
|
||||||
plexpy.INSTALL_TYPE = 'windows'
|
jellypy.INSTALL_TYPE = 'windows'
|
||||||
current_version, current_branch = get_version_from_file()
|
current_version, current_branch = get_version_from_file()
|
||||||
return current_version, 'origin', current_branch
|
return current_version, 'origin', current_branch
|
||||||
|
|
||||||
elif plexpy.FROZEN and common.PLATFORM == 'Darwin':
|
elif jellypy.FROZEN and common.PLATFORM == 'Darwin':
|
||||||
plexpy.INSTALL_TYPE = 'macos'
|
jellypy.INSTALL_TYPE = 'macos'
|
||||||
current_version, current_branch = get_version_from_file()
|
current_version, current_branch = get_version_from_file()
|
||||||
return current_version, 'origin', current_branch
|
return current_version, 'origin', current_branch
|
||||||
|
|
||||||
elif os.path.isdir(os.path.join(plexpy.PROG_DIR, '.git')):
|
elif os.path.isdir(os.path.join(jellypy.PROG_DIR, '.git')):
|
||||||
plexpy.INSTALL_TYPE = 'git'
|
jellypy.INSTALL_TYPE = 'git'
|
||||||
output, err = runGit('rev-parse HEAD')
|
output, err = runGit('rev-parse HEAD')
|
||||||
|
|
||||||
if not output:
|
if not output:
|
||||||
@@ -104,9 +104,9 @@ def get_version():
|
|||||||
logger.error('Output does not look like a hash, not using it.')
|
logger.error('Output does not look like a hash, not using it.')
|
||||||
cur_commit_hash = None
|
cur_commit_hash = None
|
||||||
|
|
||||||
if plexpy.CONFIG.DO_NOT_OVERRIDE_GIT_BRANCH and plexpy.CONFIG.GIT_BRANCH:
|
if jellypy.CONFIG.DO_NOT_OVERRIDE_GIT_BRANCH and jellypy.CONFIG.GIT_BRANCH:
|
||||||
remote_name = None
|
remote_name = None
|
||||||
branch_name = plexpy.CONFIG.GIT_BRANCH
|
branch_name = jellypy.CONFIG.GIT_BRANCH
|
||||||
|
|
||||||
else:
|
else:
|
||||||
remote_branch, err = runGit('rev-parse --abbrev-ref --symbolic-full-name @{u}')
|
remote_branch, err = runGit('rev-parse --abbrev-ref --symbolic-full-name @{u}')
|
||||||
@@ -116,16 +116,16 @@ def get_version():
|
|||||||
else:
|
else:
|
||||||
remote_name = branch_name = None
|
remote_name = branch_name = None
|
||||||
|
|
||||||
if not remote_name and plexpy.CONFIG.GIT_REMOTE:
|
if not remote_name and jellypy.CONFIG.GIT_REMOTE:
|
||||||
logger.error('Could not retrieve remote name from git. Falling back to %s.' % plexpy.CONFIG.GIT_REMOTE)
|
logger.error('Could not retrieve remote name from git. Falling back to %s.' % jellypy.CONFIG.GIT_REMOTE)
|
||||||
remote_name = plexpy.CONFIG.GIT_REMOTE
|
remote_name = jellypy.CONFIG.GIT_REMOTE
|
||||||
if not remote_name:
|
if not remote_name:
|
||||||
logger.error('Could not retrieve remote name from git. Defaulting to origin.')
|
logger.error('Could not retrieve remote name from git. Defaulting to origin.')
|
||||||
branch_name = 'origin'
|
branch_name = 'origin'
|
||||||
|
|
||||||
if not branch_name and plexpy.CONFIG.GIT_BRANCH:
|
if not branch_name and jellypy.CONFIG.GIT_BRANCH:
|
||||||
logger.error('Could not retrieve branch name from git. Falling back to %s.' % plexpy.CONFIG.GIT_BRANCH)
|
logger.error('Could not retrieve branch name from git. Falling back to %s.' % jellypy.CONFIG.GIT_BRANCH)
|
||||||
branch_name = plexpy.CONFIG.GIT_BRANCH
|
branch_name = jellypy.CONFIG.GIT_BRANCH
|
||||||
if not branch_name:
|
if not branch_name:
|
||||||
logger.error('Could not retrieve branch name from git. Defaulting to master.')
|
logger.error('Could not retrieve branch name from git. Defaulting to master.')
|
||||||
branch_name = 'master'
|
branch_name = 'master'
|
||||||
@@ -133,20 +133,20 @@ def get_version():
|
|||||||
return cur_commit_hash, remote_name, branch_name
|
return cur_commit_hash, remote_name, branch_name
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if plexpy.DOCKER:
|
if jellypy.DOCKER:
|
||||||
plexpy.INSTALL_TYPE = 'docker'
|
jellypy.INSTALL_TYPE = 'docker'
|
||||||
elif plexpy.SNAP:
|
elif jellypy.SNAP:
|
||||||
plexpy.INSTALL_TYPE = 'snap'
|
jellypy.INSTALL_TYPE = 'snap'
|
||||||
else:
|
else:
|
||||||
plexpy.INSTALL_TYPE = 'source'
|
jellypy.INSTALL_TYPE = 'source'
|
||||||
|
|
||||||
current_version, current_branch = get_version_from_file()
|
current_version, current_branch = get_version_from_file()
|
||||||
return current_version, 'origin', current_branch
|
return current_version, 'origin', current_branch
|
||||||
|
|
||||||
|
|
||||||
def get_version_from_file():
|
def get_version_from_file():
|
||||||
version_file = os.path.join(plexpy.PROG_DIR, 'version.txt')
|
version_file = os.path.join(jellypy.PROG_DIR, 'version.txt')
|
||||||
branch_file = os.path.join(plexpy.PROG_DIR, 'branch.txt')
|
branch_file = os.path.join(jellypy.PROG_DIR, 'branch.txt')
|
||||||
|
|
||||||
if os.path.isfile(version_file):
|
if os.path.isfile(version_file):
|
||||||
with open(version_file, 'r') as f:
|
with open(version_file, 'r') as f:
|
||||||
@@ -166,30 +166,30 @@ def get_version_from_file():
|
|||||||
def check_update(scheduler=False, notify=False, use_cache=False):
|
def check_update(scheduler=False, notify=False, use_cache=False):
|
||||||
check_github(scheduler=scheduler, notify=notify, use_cache=use_cache)
|
check_github(scheduler=scheduler, notify=notify, use_cache=use_cache)
|
||||||
|
|
||||||
if not plexpy.CURRENT_VERSION:
|
if not jellypy.CURRENT_VERSION:
|
||||||
plexpy.UPDATE_AVAILABLE = None
|
jellypy.UPDATE_AVAILABLE = None
|
||||||
elif plexpy.COMMITS_BEHIND > 0 and \
|
elif jellypy.COMMITS_BEHIND > 0 and \
|
||||||
(plexpy.common.BRANCH in ('master', 'beta') or plexpy.SNAP or plexpy.FROZEN) and \
|
(jellypy.common.BRANCH in ('master', 'beta') or jellypy.SNAP or jellypy.FROZEN) and \
|
||||||
plexpy.common.RELEASE != plexpy.LATEST_RELEASE:
|
jellypy.common.RELEASE != jellypy.LATEST_RELEASE:
|
||||||
plexpy.UPDATE_AVAILABLE = 'release'
|
jellypy.UPDATE_AVAILABLE = 'release'
|
||||||
elif plexpy.COMMITS_BEHIND > 0 and \
|
elif jellypy.COMMITS_BEHIND > 0 and \
|
||||||
not plexpy.SNAP and not plexpy.FROZEN and \
|
not jellypy.SNAP and not jellypy.FROZEN and \
|
||||||
plexpy.CURRENT_VERSION != plexpy.LATEST_VERSION:
|
jellypy.CURRENT_VERSION != jellypy.LATEST_VERSION:
|
||||||
plexpy.UPDATE_AVAILABLE = 'commit'
|
jellypy.UPDATE_AVAILABLE = 'commit'
|
||||||
else:
|
else:
|
||||||
plexpy.UPDATE_AVAILABLE = False
|
jellypy.UPDATE_AVAILABLE = False
|
||||||
|
|
||||||
if plexpy.WIN_SYS_TRAY_ICON:
|
if jellypy.WIN_SYS_TRAY_ICON:
|
||||||
plexpy.WIN_SYS_TRAY_ICON.change_tray_update_icon()
|
jellypy.WIN_SYS_TRAY_ICON.change_tray_update_icon()
|
||||||
elif plexpy.MAC_SYS_TRAY_ICON:
|
elif jellypy.MAC_SYS_TRAY_ICON:
|
||||||
plexpy.MAC_SYS_TRAY_ICON.change_tray_update_icon()
|
jellypy.MAC_SYS_TRAY_ICON.change_tray_update_icon()
|
||||||
|
|
||||||
|
|
||||||
def check_github(scheduler=False, notify=False, use_cache=False):
|
def check_github(scheduler=False, notify=False, use_cache=False):
|
||||||
plexpy.COMMITS_BEHIND = 0
|
jellypy.COMMITS_BEHIND = 0
|
||||||
|
|
||||||
if plexpy.CONFIG.GIT_TOKEN:
|
if jellypy.CONFIG.GIT_TOKEN:
|
||||||
headers = {'Authorization': 'token {}'.format(plexpy.CONFIG.GIT_TOKEN)}
|
headers = {'Authorization': 'token {}'.format(jellypy.CONFIG.GIT_TOKEN)}
|
||||||
else:
|
else:
|
||||||
headers = {}
|
headers = {}
|
||||||
|
|
||||||
@@ -197,118 +197,118 @@ def check_github(scheduler=False, notify=False, use_cache=False):
|
|||||||
if not version:
|
if not version:
|
||||||
# Get the latest version available from github
|
# Get the latest version available from github
|
||||||
logger.info('Retrieving latest version information from GitHub')
|
logger.info('Retrieving latest version information from GitHub')
|
||||||
url = 'https://api.github.com/repos/%s/%s/commits/%s' % (plexpy.CONFIG.GIT_USER,
|
url = 'https://api.github.com/repos/%s/%s/commits/%s' % (jellypy.CONFIG.GIT_USER,
|
||||||
plexpy.CONFIG.GIT_REPO,
|
jellypy.CONFIG.GIT_REPO,
|
||||||
plexpy.CONFIG.GIT_BRANCH)
|
jellypy.CONFIG.GIT_BRANCH)
|
||||||
version = request.request_json(url, headers=headers, timeout=20,
|
version = request.request_json(url, headers=headers, timeout=20,
|
||||||
validator=lambda x: type(x) == dict)
|
validator=lambda x: type(x) == dict)
|
||||||
github_cache('version', github_data=version)
|
github_cache('version', github_data=version)
|
||||||
|
|
||||||
if version is None:
|
if version is None:
|
||||||
logger.warn('Could not get the latest version from GitHub. Are you running a local development version?')
|
logger.warn('Could not get the latest version from GitHub. Are you running a local development version?')
|
||||||
return plexpy.CURRENT_VERSION
|
return jellypy.CURRENT_VERSION
|
||||||
|
|
||||||
plexpy.LATEST_VERSION = version['sha']
|
jellypy.LATEST_VERSION = version['sha']
|
||||||
logger.debug("Latest version is %s", plexpy.LATEST_VERSION)
|
logger.debug("Latest version is %s", jellypy.LATEST_VERSION)
|
||||||
|
|
||||||
# See how many commits behind we are
|
# See how many commits behind we are
|
||||||
if not plexpy.CURRENT_VERSION:
|
if not jellypy.CURRENT_VERSION:
|
||||||
logger.info('You are running an unknown version of Tautulli. Run the updater to identify your version')
|
logger.info('You are running an unknown version of Tautulli. Run the updater to identify your version')
|
||||||
return plexpy.LATEST_VERSION
|
return jellypy.LATEST_VERSION
|
||||||
|
|
||||||
if plexpy.LATEST_VERSION == plexpy.CURRENT_VERSION:
|
if jellypy.LATEST_VERSION == jellypy.CURRENT_VERSION:
|
||||||
logger.info('Tautulli is up to date')
|
logger.info('Tautulli is up to date')
|
||||||
return plexpy.LATEST_VERSION
|
return jellypy.LATEST_VERSION
|
||||||
|
|
||||||
commits = github_cache('commits', use_cache=use_cache)
|
commits = github_cache('commits', use_cache=use_cache)
|
||||||
if not commits:
|
if not commits:
|
||||||
logger.info('Comparing currently installed version with latest GitHub version')
|
logger.info('Comparing currently installed version with latest GitHub version')
|
||||||
url = 'https://api.github.com/repos/%s/%s/compare/%s...%s' % (plexpy.CONFIG.GIT_USER,
|
url = 'https://api.github.com/repos/%s/%s/compare/%s...%s' % (jellypy.CONFIG.GIT_USER,
|
||||||
plexpy.CONFIG.GIT_REPO,
|
jellypy.CONFIG.GIT_REPO,
|
||||||
plexpy.LATEST_VERSION,
|
jellypy.LATEST_VERSION,
|
||||||
plexpy.CURRENT_VERSION)
|
jellypy.CURRENT_VERSION)
|
||||||
commits = request.request_json(url, headers=headers, timeout=20, whitelist_status_code=404,
|
commits = request.request_json(url, headers=headers, timeout=20, whitelist_status_code=404,
|
||||||
validator=lambda x: type(x) == dict)
|
validator=lambda x: type(x) == dict)
|
||||||
github_cache('commits', github_data=commits)
|
github_cache('commits', github_data=commits)
|
||||||
|
|
||||||
if commits is None:
|
if commits is None:
|
||||||
logger.warn('Could not get commits behind from GitHub.')
|
logger.warn('Could not get commits behind from GitHub.')
|
||||||
return plexpy.LATEST_VERSION
|
return jellypy.LATEST_VERSION
|
||||||
|
|
||||||
try:
|
try:
|
||||||
plexpy.COMMITS_BEHIND = int(commits['behind_by'])
|
jellypy.COMMITS_BEHIND = int(commits['behind_by'])
|
||||||
logger.debug("In total, %d commits behind", plexpy.COMMITS_BEHIND)
|
logger.debug("In total, %d commits behind", jellypy.COMMITS_BEHIND)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
logger.info('Cannot compare versions. Are you running a local development version?')
|
logger.info('Cannot compare versions. Are you running a local development version?')
|
||||||
plexpy.COMMITS_BEHIND = 0
|
jellypy.COMMITS_BEHIND = 0
|
||||||
|
|
||||||
if plexpy.COMMITS_BEHIND > 0:
|
if jellypy.COMMITS_BEHIND > 0:
|
||||||
logger.info('New version is available. You are %s commits behind' % plexpy.COMMITS_BEHIND)
|
logger.info('New version is available. You are %s commits behind' % jellypy.COMMITS_BEHIND)
|
||||||
|
|
||||||
releases = github_cache('releases', use_cache=use_cache)
|
releases = github_cache('releases', use_cache=use_cache)
|
||||||
if not releases:
|
if not releases:
|
||||||
url = 'https://api.github.com/repos/%s/%s/releases' % (plexpy.CONFIG.GIT_USER,
|
url = 'https://api.github.com/repos/%s/%s/releases' % (jellypy.CONFIG.GIT_USER,
|
||||||
plexpy.CONFIG.GIT_REPO)
|
jellypy.CONFIG.GIT_REPO)
|
||||||
releases = request.request_json(url, timeout=20, whitelist_status_code=404,
|
releases = request.request_json(url, timeout=20, whitelist_status_code=404,
|
||||||
validator=lambda x: type(x) == list)
|
validator=lambda x: type(x) == list)
|
||||||
github_cache('releases', github_data=releases)
|
github_cache('releases', github_data=releases)
|
||||||
|
|
||||||
if releases is None:
|
if releases is None:
|
||||||
logger.warn('Could not get releases from GitHub.')
|
logger.warn('Could not get releases from GitHub.')
|
||||||
return plexpy.LATEST_VERSION
|
return jellypy.LATEST_VERSION
|
||||||
|
|
||||||
if plexpy.CONFIG.GIT_BRANCH == 'master':
|
if jellypy.CONFIG.GIT_BRANCH == 'master':
|
||||||
release = next((r for r in releases if not r['prerelease']), releases[0])
|
release = next((r for r in releases if not r['prerelease']), releases[0])
|
||||||
elif plexpy.CONFIG.GIT_BRANCH == 'beta':
|
elif jellypy.CONFIG.GIT_BRANCH == 'beta':
|
||||||
release = next((r for r in releases if not r['tag_name'].endswith('-nightly')), releases[0])
|
release = next((r for r in releases if not r['tag_name'].endswith('-nightly')), releases[0])
|
||||||
elif plexpy.CONFIG.GIT_BRANCH == 'nightly':
|
elif jellypy.CONFIG.GIT_BRANCH == 'nightly':
|
||||||
release = next((r for r in releases), releases[0])
|
release = next((r for r in releases), releases[0])
|
||||||
else:
|
else:
|
||||||
release = releases[0]
|
release = releases[0]
|
||||||
|
|
||||||
plexpy.LATEST_RELEASE = release['tag_name']
|
jellypy.LATEST_RELEASE = release['tag_name']
|
||||||
|
|
||||||
if notify:
|
if notify:
|
||||||
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_plexpyupdate',
|
jellypy.NOTIFY_QUEUE.put({'notify_action': 'on_plexpyupdate',
|
||||||
'plexpy_download_info': release,
|
'plexpy_download_info': release,
|
||||||
'plexpy_update_commit': plexpy.LATEST_VERSION,
|
'plexpy_update_commit': jellypy.LATEST_VERSION,
|
||||||
'plexpy_update_behind': plexpy.COMMITS_BEHIND})
|
'plexpy_update_behind': jellypy.COMMITS_BEHIND})
|
||||||
|
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
logger.warn('Tautulli is running using Python 2. Unable to run automatic update.')
|
logger.warn('Tautulli is running using Python 2. Unable to run automatic update.')
|
||||||
|
|
||||||
elif scheduler and plexpy.CONFIG.PLEXPY_AUTO_UPDATE and \
|
elif scheduler and jellypy.CONFIG.PLEXPY_AUTO_UPDATE and \
|
||||||
not plexpy.DOCKER and not plexpy.SNAP and \
|
not jellypy.DOCKER and not jellypy.SNAP and \
|
||||||
not (plexpy.FROZEN and common.PLATFORM == 'Darwin'):
|
not (jellypy.FROZEN and common.PLATFORM == 'Darwin'):
|
||||||
logger.info('Running automatic update.')
|
logger.info('Running automatic update.')
|
||||||
plexpy.shutdown(restart=True, update=True)
|
jellypy.shutdown(restart=True, update=True)
|
||||||
|
|
||||||
elif plexpy.COMMITS_BEHIND == 0:
|
elif jellypy.COMMITS_BEHIND == 0:
|
||||||
logger.info('Tautulli is up to date')
|
logger.info('Tautulli is up to date')
|
||||||
|
|
||||||
return plexpy.LATEST_VERSION
|
return jellypy.LATEST_VERSION
|
||||||
|
|
||||||
|
|
||||||
def update():
|
def update():
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
logger.warn('Tautulli is running using Python 2. Unable to update.')
|
logger.warn('Tautulli is running using Python 2. Unable to update.')
|
||||||
return
|
return
|
||||||
|
|
||||||
if not plexpy.UPDATE_AVAILABLE:
|
if not jellypy.UPDATE_AVAILABLE:
|
||||||
return
|
return
|
||||||
|
|
||||||
if plexpy.INSTALL_TYPE in ('docker', 'snap', 'macos'):
|
if jellypy.INSTALL_TYPE in ('docker', 'snap', 'macos'):
|
||||||
return
|
return
|
||||||
|
|
||||||
elif plexpy.INSTALL_TYPE == 'windows':
|
elif jellypy.INSTALL_TYPE == 'windows':
|
||||||
logger.info('Calling Windows scheduled task to update Tautulli')
|
logger.info('Calling Windows scheduled task to update Tautulli')
|
||||||
CREATE_NO_WINDOW = 0x08000000
|
CREATE_NO_WINDOW = 0x08000000
|
||||||
subprocess.Popen(['SCHTASKS', '/Run', '/TN', 'TautulliUpdateTask'],
|
subprocess.Popen(['SCHTASKS', '/Run', '/TN', 'TautulliUpdateTask'],
|
||||||
creationflags=CREATE_NO_WINDOW)
|
creationflags=CREATE_NO_WINDOW)
|
||||||
|
|
||||||
elif plexpy.INSTALL_TYPE == 'git':
|
elif jellypy.INSTALL_TYPE == 'git':
|
||||||
output, err = runGit('pull --ff-only {} {}'.format(plexpy.CONFIG.GIT_REMOTE,
|
output, err = runGit('pull --ff-only {} {}'.format(jellypy.CONFIG.GIT_REMOTE,
|
||||||
plexpy.CONFIG.GIT_BRANCH))
|
jellypy.CONFIG.GIT_BRANCH))
|
||||||
|
|
||||||
if not output:
|
if not output:
|
||||||
logger.error('Unable to download latest version')
|
logger.error('Unable to download latest version')
|
||||||
@@ -320,12 +320,12 @@ def update():
|
|||||||
elif line.endswith(('Aborting', 'Aborting.')):
|
elif line.endswith(('Aborting', 'Aborting.')):
|
||||||
logger.error('Unable to update from git: ' + line)
|
logger.error('Unable to update from git: ' + line)
|
||||||
|
|
||||||
elif plexpy.INSTALL_TYPE == 'source':
|
elif jellypy.INSTALL_TYPE == 'source':
|
||||||
tar_download_url = 'https://github.com/{}/{}/tarball/{}'.format(plexpy.CONFIG.GIT_USER,
|
tar_download_url = 'https://github.com/{}/{}/tarball/{}'.format(jellypy.CONFIG.GIT_USER,
|
||||||
plexpy.CONFIG.GIT_REPO,
|
jellypy.CONFIG.GIT_REPO,
|
||||||
plexpy.CONFIG.GIT_BRANCH)
|
jellypy.CONFIG.GIT_BRANCH)
|
||||||
update_dir = os.path.join(plexpy.DATA_DIR, 'update')
|
update_dir = os.path.join(jellypy.DATA_DIR, 'update')
|
||||||
version_path = os.path.join(plexpy.PROG_DIR, 'version.txt')
|
version_path = os.path.join(jellypy.PROG_DIR, 'version.txt')
|
||||||
|
|
||||||
logger.info('Downloading update from: ' + tar_download_url)
|
logger.info('Downloading update from: ' + tar_download_url)
|
||||||
data = request.request_content(tar_download_url)
|
data = request.request_content(tar_download_url)
|
||||||
@@ -334,8 +334,8 @@ def update():
|
|||||||
logger.error("Unable to retrieve new version from '%s', can't update", tar_download_url)
|
logger.error("Unable to retrieve new version from '%s', can't update", tar_download_url)
|
||||||
return
|
return
|
||||||
|
|
||||||
download_name = plexpy.CONFIG.GIT_BRANCH + '-github'
|
download_name = jellypy.CONFIG.GIT_BRANCH + '-github'
|
||||||
tar_download_path = os.path.join(plexpy.DATA_DIR, download_name)
|
tar_download_path = os.path.join(jellypy.DATA_DIR, download_name)
|
||||||
|
|
||||||
# Save tar to disk
|
# Save tar to disk
|
||||||
with open(tar_download_path, 'wb') as f:
|
with open(tar_download_path, 'wb') as f:
|
||||||
@@ -363,7 +363,7 @@ def update():
|
|||||||
dirname = dirname[len(content_dir) + 1:]
|
dirname = dirname[len(content_dir) + 1:]
|
||||||
for curfile in filenames:
|
for curfile in filenames:
|
||||||
old_path = os.path.join(content_dir, dirname, curfile)
|
old_path = os.path.join(content_dir, dirname, curfile)
|
||||||
new_path = os.path.join(plexpy.PROG_DIR, dirname, curfile)
|
new_path = os.path.join(jellypy.PROG_DIR, dirname, curfile)
|
||||||
|
|
||||||
if os.path.isfile(new_path):
|
if os.path.isfile(new_path):
|
||||||
os.remove(new_path)
|
os.remove(new_path)
|
||||||
@@ -372,7 +372,7 @@ def update():
|
|||||||
# Update version.txt
|
# Update version.txt
|
||||||
try:
|
try:
|
||||||
with open(version_path, 'w') as f:
|
with open(version_path, 'w') as f:
|
||||||
f.write(str(plexpy.LATEST_VERSION))
|
f.write(str(jellypy.LATEST_VERSION))
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
logger.error(
|
logger.error(
|
||||||
"Unable to write current version to version.txt, update not complete: %s",
|
"Unable to write current version to version.txt, update not complete: %s",
|
||||||
@@ -382,18 +382,18 @@ def update():
|
|||||||
|
|
||||||
|
|
||||||
def reset_git_install():
|
def reset_git_install():
|
||||||
if plexpy.INSTALL_TYPE == 'git':
|
if jellypy.INSTALL_TYPE == 'git':
|
||||||
logger.info('Attempting to reset git install to "{}/{}/{}"'.format(plexpy.CONFIG.GIT_REMOTE,
|
logger.info('Attempting to reset git install to "{}/{}/{}"'.format(jellypy.CONFIG.GIT_REMOTE,
|
||||||
plexpy.CONFIG.GIT_BRANCH,
|
jellypy.CONFIG.GIT_BRANCH,
|
||||||
common.RELEASE))
|
common.RELEASE))
|
||||||
|
|
||||||
output, err = runGit('remote set-url {} https://github.com/{}/{}.git'.format(plexpy.CONFIG.GIT_REMOTE,
|
output, err = runGit('remote set-url {} https://github.com/{}/{}.git'.format(jellypy.CONFIG.GIT_REMOTE,
|
||||||
plexpy.CONFIG.GIT_USER,
|
jellypy.CONFIG.GIT_USER,
|
||||||
plexpy.CONFIG.GIT_REPO))
|
jellypy.CONFIG.GIT_REPO))
|
||||||
output, err = runGit('fetch {}'.format(plexpy.CONFIG.GIT_REMOTE))
|
output, err = runGit('fetch {}'.format(jellypy.CONFIG.GIT_REMOTE))
|
||||||
output, err = runGit('checkout {}'.format(plexpy.CONFIG.GIT_BRANCH))
|
output, err = runGit('checkout {}'.format(jellypy.CONFIG.GIT_BRANCH))
|
||||||
output, err = runGit('branch -u {}/{}'.format(plexpy.CONFIG.GIT_REMOTE,
|
output, err = runGit('branch -u {}/{}'.format(jellypy.CONFIG.GIT_REMOTE,
|
||||||
plexpy.CONFIG.GIT_BRANCH))
|
jellypy.CONFIG.GIT_BRANCH))
|
||||||
output, err = runGit('reset --hard {}'.format(common.RELEASE))
|
output, err = runGit('reset --hard {}'.format(common.RELEASE))
|
||||||
|
|
||||||
if not output:
|
if not output:
|
||||||
@@ -410,12 +410,12 @@ def reset_git_install():
|
|||||||
|
|
||||||
|
|
||||||
def checkout_git_branch():
|
def checkout_git_branch():
|
||||||
if plexpy.INSTALL_TYPE == 'git':
|
if jellypy.INSTALL_TYPE == 'git':
|
||||||
logger.info('Attempting to checkout git branch "{}/{}"'.format(plexpy.CONFIG.GIT_REMOTE,
|
logger.info('Attempting to checkout git branch "{}/{}"'.format(jellypy.CONFIG.GIT_REMOTE,
|
||||||
plexpy.CONFIG.GIT_BRANCH))
|
jellypy.CONFIG.GIT_BRANCH))
|
||||||
|
|
||||||
output, err = runGit('fetch {}'.format(plexpy.CONFIG.GIT_REMOTE))
|
output, err = runGit('fetch {}'.format(jellypy.CONFIG.GIT_REMOTE))
|
||||||
output, err = runGit('checkout {}'.format(plexpy.CONFIG.GIT_BRANCH))
|
output, err = runGit('checkout {}'.format(jellypy.CONFIG.GIT_BRANCH))
|
||||||
|
|
||||||
if not output:
|
if not output:
|
||||||
logger.error('Unable to change git branch.')
|
logger.error('Unable to change git branch.')
|
||||||
@@ -426,13 +426,13 @@ def checkout_git_branch():
|
|||||||
logger.error('Unable to checkout from git: ' + line)
|
logger.error('Unable to checkout from git: ' + line)
|
||||||
return
|
return
|
||||||
|
|
||||||
output, err = runGit('pull {} {}'.format(plexpy.CONFIG.GIT_REMOTE,
|
output, err = runGit('pull {} {}'.format(jellypy.CONFIG.GIT_REMOTE,
|
||||||
plexpy.CONFIG.GIT_BRANCH))
|
jellypy.CONFIG.GIT_BRANCH))
|
||||||
|
|
||||||
|
|
||||||
def github_cache(cache, github_data=None, use_cache=True):
|
def github_cache(cache, github_data=None, use_cache=True):
|
||||||
timestamp = helpers.timestamp()
|
timestamp = helpers.timestamp()
|
||||||
cache_filepath = os.path.join(plexpy.CONFIG.CACHE_DIR, 'github_{}.json'.format(cache))
|
cache_filepath = os.path.join(jellypy.CONFIG.CACHE_DIR, 'github_{}.json'.format(cache))
|
||||||
|
|
||||||
if github_data:
|
if github_data:
|
||||||
cache_data = {'github_data': github_data, '_cache_time': timestamp}
|
cache_data = {'github_data': github_data, '_cache_time': timestamp}
|
||||||
@@ -447,7 +447,7 @@ def github_cache(cache, github_data=None, use_cache=True):
|
|||||||
try:
|
try:
|
||||||
with open(cache_filepath, 'r', encoding='utf-8') as cache_file:
|
with open(cache_filepath, 'r', encoding='utf-8') as cache_file:
|
||||||
cache_data = json.load(cache_file)
|
cache_data = json.load(cache_file)
|
||||||
if timestamp - cache_data['_cache_time'] < plexpy.CONFIG.CHECK_GITHUB_CACHE_SECONDS:
|
if timestamp - cache_data['_cache_time'] < jellypy.CONFIG.CHECK_GITHUB_CACHE_SECONDS:
|
||||||
logger.debug('Using cached GitHub %s data', cache)
|
logger.debug('Using cached GitHub %s data', cache)
|
||||||
return cache_data['github_data']
|
return cache_data['github_data']
|
||||||
except:
|
except:
|
||||||
@@ -455,7 +455,7 @@ def github_cache(cache, github_data=None, use_cache=True):
|
|||||||
|
|
||||||
|
|
||||||
def read_changelog(latest_only=False, since_prev_release=False):
|
def read_changelog(latest_only=False, since_prev_release=False):
|
||||||
changelog_file = os.path.join(plexpy.PROG_DIR, 'CHANGELOG.md')
|
changelog_file = os.path.join(jellypy.PROG_DIR, 'CHANGELOG.md')
|
||||||
|
|
||||||
if not os.path.isfile(changelog_file):
|
if not os.path.isfile(changelog_file):
|
||||||
return '<h4>Missing changelog file</h4>'
|
return '<h4>Missing changelog file</h4>'
|
||||||
@@ -470,7 +470,7 @@ def read_changelog(latest_only=False, since_prev_release=False):
|
|||||||
list_pattern = re.compile(r'(^[ \t]*\*\s)(.+)')
|
list_pattern = re.compile(r'(^[ \t]*\*\s)(.+)')
|
||||||
|
|
||||||
beta_release = False
|
beta_release = False
|
||||||
prev_release = str(plexpy.PREV_RELEASE)
|
prev_release = str(jellypy.PREV_RELEASE)
|
||||||
|
|
||||||
with open(changelog_file, "r") as logfile:
|
with open(changelog_file, "r") as logfile:
|
||||||
for line in logfile:
|
for line in logfile:
|
||||||
@@ -28,19 +28,19 @@ import time
|
|||||||
import certifi
|
import certifi
|
||||||
import websocket
|
import websocket
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
import activity_handler
|
import activity_handler
|
||||||
import activity_pinger
|
import activity_pinger
|
||||||
import activity_processor
|
import activity_processor
|
||||||
import database
|
import database
|
||||||
import logger
|
import logger
|
||||||
else:
|
else:
|
||||||
from plexpy import activity_handler
|
from jellypy import activity_handler
|
||||||
from plexpy import activity_pinger
|
from jellypy import activity_pinger
|
||||||
from plexpy import activity_processor
|
from jellypy import activity_processor
|
||||||
from plexpy import database
|
from jellypy import database
|
||||||
from plexpy import logger
|
from jellypy import logger
|
||||||
|
|
||||||
|
|
||||||
name = 'websocket'
|
name = 'websocket'
|
||||||
@@ -66,30 +66,30 @@ def start_thread():
|
|||||||
|
|
||||||
|
|
||||||
def on_connect():
|
def on_connect():
|
||||||
if plexpy.PLEX_SERVER_UP is None:
|
if jellypy.PLEX_SERVER_UP is None:
|
||||||
plexpy.PLEX_SERVER_UP = True
|
jellypy.PLEX_SERVER_UP = True
|
||||||
|
|
||||||
if not plexpy.PLEX_SERVER_UP:
|
if not jellypy.PLEX_SERVER_UP:
|
||||||
logger.info("Tautulli WebSocket :: The Plex Media Server is back up.")
|
logger.info("Tautulli WebSocket :: The Plex Media Server is back up.")
|
||||||
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_intup'})
|
jellypy.NOTIFY_QUEUE.put({'notify_action': 'on_intup'})
|
||||||
plexpy.PLEX_SERVER_UP = True
|
jellypy.PLEX_SERVER_UP = True
|
||||||
|
|
||||||
plexpy.initialize_scheduler()
|
jellypy.initialize_scheduler()
|
||||||
if plexpy.CONFIG.WEBSOCKET_MONITOR_PING_PONG:
|
if jellypy.CONFIG.WEBSOCKET_MONITOR_PING_PONG:
|
||||||
send_ping()
|
send_ping()
|
||||||
|
|
||||||
|
|
||||||
def on_disconnect():
|
def on_disconnect():
|
||||||
if plexpy.PLEX_SERVER_UP is None:
|
if jellypy.PLEX_SERVER_UP is None:
|
||||||
plexpy.PLEX_SERVER_UP = False
|
jellypy.PLEX_SERVER_UP = False
|
||||||
|
|
||||||
if plexpy.PLEX_SERVER_UP:
|
if jellypy.PLEX_SERVER_UP:
|
||||||
logger.info("Tautulli WebSocket :: Unable to get a response from the server, Plex server is down.")
|
logger.info("Tautulli WebSocket :: Unable to get a response from the server, Plex server is down.")
|
||||||
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_intdown'})
|
jellypy.NOTIFY_QUEUE.put({'notify_action': 'on_intdown'})
|
||||||
plexpy.PLEX_SERVER_UP = False
|
jellypy.PLEX_SERVER_UP = False
|
||||||
|
|
||||||
activity_processor.ActivityProcessor().set_temp_stopped()
|
activity_processor.ActivityProcessor().set_temp_stopped()
|
||||||
plexpy.initialize_scheduler()
|
jellypy.initialize_scheduler()
|
||||||
|
|
||||||
|
|
||||||
def reconnect():
|
def reconnect():
|
||||||
@@ -106,14 +106,14 @@ def shutdown():
|
|||||||
|
|
||||||
def close():
|
def close():
|
||||||
logger.info("Tautulli WebSocket :: Disconnecting websocket...")
|
logger.info("Tautulli WebSocket :: Disconnecting websocket...")
|
||||||
plexpy.WEBSOCKET.close()
|
jellypy.WEBSOCKET.close()
|
||||||
plexpy.WS_CONNECTED = False
|
jellypy.WS_CONNECTED = False
|
||||||
|
|
||||||
|
|
||||||
def send_ping():
|
def send_ping():
|
||||||
if plexpy.WS_CONNECTED:
|
if jellypy.WS_CONNECTED:
|
||||||
# logger.debug("Tautulli WebSocket :: Sending ping.")
|
# logger.debug("Tautulli WebSocket :: Sending ping.")
|
||||||
plexpy.WEBSOCKET.ping("Hi?")
|
jellypy.WEBSOCKET.ping("Hi?")
|
||||||
|
|
||||||
global pong_timer
|
global pong_timer
|
||||||
pong_timer = threading.Timer(5.0, wait_pong)
|
pong_timer = threading.Timer(5.0, wait_pong)
|
||||||
@@ -127,7 +127,7 @@ def wait_pong():
|
|||||||
|
|
||||||
logger.warn("Tautulli WebSocket :: Failed to receive pong from websocket, ping attempt %s." % str(pong_count))
|
logger.warn("Tautulli WebSocket :: Failed to receive pong from websocket, ping attempt %s." % str(pong_count))
|
||||||
|
|
||||||
if pong_count >= plexpy.CONFIG.WEBSOCKET_CONNECTION_ATTEMPTS:
|
if pong_count >= jellypy.CONFIG.WEBSOCKET_CONNECTION_ATTEMPTS:
|
||||||
pong_count = 0
|
pong_count = 0
|
||||||
close()
|
close()
|
||||||
|
|
||||||
@@ -144,24 +144,24 @@ def receive_pong():
|
|||||||
def run():
|
def run():
|
||||||
from websocket import create_connection
|
from websocket import create_connection
|
||||||
|
|
||||||
if plexpy.CONFIG.PMS_SSL and plexpy.CONFIG.PMS_URL[:5] == 'https':
|
if jellypy.CONFIG.PMS_SSL and jellypy.CONFIG.PMS_URL[:5] == 'https':
|
||||||
uri = plexpy.CONFIG.PMS_URL.replace('https://', 'wss://') + '/:/websockets/notifications'
|
uri = jellypy.CONFIG.PMS_URL.replace('https://', 'wss://') + '/:/websockets/notifications'
|
||||||
secure = 'secure '
|
secure = 'secure '
|
||||||
if plexpy.CONFIG.VERIFY_SSL_CERT:
|
if jellypy.CONFIG.VERIFY_SSL_CERT:
|
||||||
sslopt = {'ca_certs': certifi.where()}
|
sslopt = {'ca_certs': certifi.where()}
|
||||||
else:
|
else:
|
||||||
sslopt = {'cert_reqs': ssl.CERT_NONE}
|
sslopt = {'cert_reqs': ssl.CERT_NONE}
|
||||||
else:
|
else:
|
||||||
uri = 'ws://%s:%s/:/websockets/notifications' % (
|
uri = 'ws://%s:%s/:/websockets/notifications' % (
|
||||||
plexpy.CONFIG.PMS_IP,
|
jellypy.CONFIG.PMS_IP,
|
||||||
plexpy.CONFIG.PMS_PORT
|
jellypy.CONFIG.PMS_PORT
|
||||||
)
|
)
|
||||||
secure = ''
|
secure = ''
|
||||||
sslopt = None
|
sslopt = None
|
||||||
|
|
||||||
# Set authentication token (if one is available)
|
# Set authentication token (if one is available)
|
||||||
if plexpy.CONFIG.PMS_TOKEN:
|
if jellypy.CONFIG.PMS_TOKEN:
|
||||||
header = ["X-Plex-Token: %s" % plexpy.CONFIG.PMS_TOKEN]
|
header = ["X-Plex-Token: %s" % jellypy.CONFIG.PMS_TOKEN]
|
||||||
else:
|
else:
|
||||||
header = []
|
header = []
|
||||||
|
|
||||||
@@ -172,18 +172,18 @@ def run():
|
|||||||
# Try an open the websocket connection
|
# Try an open the websocket connection
|
||||||
logger.info("Tautulli WebSocket :: Opening %swebsocket." % secure)
|
logger.info("Tautulli WebSocket :: Opening %swebsocket." % secure)
|
||||||
try:
|
try:
|
||||||
plexpy.WEBSOCKET = create_connection(uri, header=header, sslopt=sslopt)
|
jellypy.WEBSOCKET = create_connection(uri, header=header, sslopt=sslopt)
|
||||||
logger.info("Tautulli WebSocket :: Ready")
|
logger.info("Tautulli WebSocket :: Ready")
|
||||||
plexpy.WS_CONNECTED = True
|
jellypy.WS_CONNECTED = True
|
||||||
except (websocket.WebSocketException, IOError, Exception) as e:
|
except (websocket.WebSocketException, IOError, Exception) as e:
|
||||||
logger.error("Tautulli WebSocket :: %s.", e)
|
logger.error("Tautulli WebSocket :: %s.", e)
|
||||||
|
|
||||||
if plexpy.WS_CONNECTED:
|
if jellypy.WS_CONNECTED:
|
||||||
on_connect()
|
on_connect()
|
||||||
|
|
||||||
while plexpy.WS_CONNECTED:
|
while jellypy.WS_CONNECTED:
|
||||||
try:
|
try:
|
||||||
process(*receive(plexpy.WEBSOCKET))
|
process(*receive(jellypy.WEBSOCKET))
|
||||||
|
|
||||||
# successfully received data, reset reconnects counter
|
# successfully received data, reset reconnects counter
|
||||||
reconnects = 0
|
reconnects = 0
|
||||||
@@ -195,19 +195,19 @@ def run():
|
|||||||
if reconnects == 0:
|
if reconnects == 0:
|
||||||
logger.warn("Tautulli WebSocket :: Connection has closed.")
|
logger.warn("Tautulli WebSocket :: Connection has closed.")
|
||||||
|
|
||||||
if not plexpy.CONFIG.PMS_IS_CLOUD and reconnects < plexpy.CONFIG.WEBSOCKET_CONNECTION_ATTEMPTS:
|
if not jellypy.CONFIG.PMS_IS_CLOUD and reconnects < jellypy.CONFIG.WEBSOCKET_CONNECTION_ATTEMPTS:
|
||||||
reconnects += 1
|
reconnects += 1
|
||||||
|
|
||||||
# Sleep 5 between connection attempts
|
# Sleep 5 between connection attempts
|
||||||
if reconnects > 1:
|
if reconnects > 1:
|
||||||
time.sleep(plexpy.CONFIG.WEBSOCKET_CONNECTION_TIMEOUT)
|
time.sleep(jellypy.CONFIG.WEBSOCKET_CONNECTION_TIMEOUT)
|
||||||
|
|
||||||
logger.warn("Tautulli WebSocket :: Reconnection attempt %s." % str(reconnects))
|
logger.warn("Tautulli WebSocket :: Reconnection attempt %s." % str(reconnects))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
plexpy.WEBSOCKET = create_connection(uri, header=header)
|
jellypy.WEBSOCKET = create_connection(uri, header=header)
|
||||||
logger.info("Tautulli WebSocket :: Ready")
|
logger.info("Tautulli WebSocket :: Ready")
|
||||||
plexpy.WS_CONNECTED = True
|
jellypy.WS_CONNECTED = True
|
||||||
except (websocket.WebSocketException, IOError, Exception) as e:
|
except (websocket.WebSocketException, IOError, Exception) as e:
|
||||||
logger.error("Tautulli WebSocket :: %s.", e)
|
logger.error("Tautulli WebSocket :: %s.", e)
|
||||||
|
|
||||||
@@ -223,7 +223,7 @@ def run():
|
|||||||
close()
|
close()
|
||||||
break
|
break
|
||||||
|
|
||||||
if not plexpy.WS_CONNECTED and not ws_shutdown:
|
if not jellypy.WS_CONNECTED and not ws_shutdown:
|
||||||
on_disconnect()
|
on_disconnect()
|
||||||
|
|
||||||
logger.debug("Tautulli WebSocket :: Leaving thread.")
|
logger.debug("Tautulli WebSocket :: Leaving thread.")
|
||||||
@@ -29,19 +29,19 @@ import cherrypy
|
|||||||
from hashing_passwords import check_hash
|
from hashing_passwords import check_hash
|
||||||
import jwt
|
import jwt
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
import logger
|
import logger
|
||||||
from database import MonitorDatabase
|
from database import MonitorDatabase
|
||||||
from helpers import timestamp
|
from helpers import timestamp
|
||||||
from users import Users, refresh_users
|
from users import Users, refresh_users
|
||||||
from plextv import PlexTV
|
from plextv import PlexTV
|
||||||
else:
|
else:
|
||||||
from plexpy import logger
|
from jellypy import logger
|
||||||
from plexpy.database import MonitorDatabase
|
from jellypy.database import MonitorDatabase
|
||||||
from plexpy.helpers import timestamp
|
from jellypy.helpers import timestamp
|
||||||
from plexpy.users import Users, refresh_users
|
from jellypy.users import Users, refresh_users
|
||||||
from plexpy.plextv import PlexTV
|
from jellypy.plextv import PlexTV
|
||||||
|
|
||||||
# Monkey patch SameSite support into cookies.
|
# Monkey patch SameSite support into cookies.
|
||||||
# https://stackoverflow.com/a/50813092
|
# https://stackoverflow.com/a/50813092
|
||||||
@@ -83,7 +83,7 @@ def plex_user_login(username=None, password=None, token=None, headers=None):
|
|||||||
if user_id != str(user_details['user_id']):
|
if user_id != str(user_details['user_id']):
|
||||||
# The user is not in the database.
|
# The user is not in the database.
|
||||||
return None
|
return None
|
||||||
elif plexpy.CONFIG.HTTP_PLEX_ADMIN and user_details['is_admin']:
|
elif jellypy.CONFIG.HTTP_PLEX_ADMIN and user_details['is_admin']:
|
||||||
# Plex admin login
|
# Plex admin login
|
||||||
return user_details, 'admin'
|
return user_details, 'admin'
|
||||||
elif not user_details['allow_guest'] or user_details['deleted_user']:
|
elif not user_details['allow_guest'] or user_details['deleted_user']:
|
||||||
@@ -91,7 +91,7 @@ def plex_user_login(username=None, password=None, token=None, headers=None):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# Stop here if guest access is not enabled
|
# Stop here if guest access is not enabled
|
||||||
if not plexpy.CONFIG.ALLOW_GUEST_ACCESS:
|
if not jellypy.CONFIG.ALLOW_GUEST_ACCESS:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# The user is in the database, and guest access is enabled, so try to retrieve a server token.
|
# The user is in the database, and guest access is enabled, so try to retrieve a server token.
|
||||||
@@ -139,17 +139,17 @@ def check_credentials(username=None, password=None, token=None, admin_login='0',
|
|||||||
Returns True and the user group on success or False and no user group"""
|
Returns True and the user group on success or False and no user group"""
|
||||||
|
|
||||||
if username and password:
|
if username and password:
|
||||||
if plexpy.CONFIG.HTTP_PASSWORD:
|
if jellypy.CONFIG.HTTP_PASSWORD:
|
||||||
user_details = {'user_id': None, 'username': username}
|
user_details = {'user_id': None, 'username': username}
|
||||||
|
|
||||||
if plexpy.CONFIG.HTTP_HASHED_PASSWORD and \
|
if jellypy.CONFIG.HTTP_HASHED_PASSWORD and \
|
||||||
username == plexpy.CONFIG.HTTP_USERNAME and check_hash(password, plexpy.CONFIG.HTTP_PASSWORD):
|
username == jellypy.CONFIG.HTTP_USERNAME and check_hash(password, jellypy.CONFIG.HTTP_PASSWORD):
|
||||||
return True, user_details, 'admin'
|
return True, user_details, 'admin'
|
||||||
elif not plexpy.CONFIG.HTTP_HASHED_PASSWORD and \
|
elif not jellypy.CONFIG.HTTP_HASHED_PASSWORD and \
|
||||||
username == plexpy.CONFIG.HTTP_USERNAME and password == plexpy.CONFIG.HTTP_PASSWORD:
|
username == jellypy.CONFIG.HTTP_USERNAME and password == jellypy.CONFIG.HTTP_PASSWORD:
|
||||||
return True, user_details, 'admin'
|
return True, user_details, 'admin'
|
||||||
|
|
||||||
if plexpy.CONFIG.HTTP_PLEX_ADMIN or (not admin_login == '1' and plexpy.CONFIG.ALLOW_GUEST_ACCESS):
|
if jellypy.CONFIG.HTTP_PLEX_ADMIN or (not admin_login == '1' and jellypy.CONFIG.ALLOW_GUEST_ACCESS):
|
||||||
plex_login = plex_user_login(token=token, headers=headers)
|
plex_login = plex_user_login(token=token, headers=headers)
|
||||||
if plex_login is not None:
|
if plex_login is not None:
|
||||||
return True, plex_login[0], plex_login[1]
|
return True, plex_login[0], plex_login[1]
|
||||||
@@ -158,13 +158,13 @@ def check_credentials(username=None, password=None, token=None, admin_login='0',
|
|||||||
|
|
||||||
|
|
||||||
def check_jwt_token():
|
def check_jwt_token():
|
||||||
jwt_cookie = str(JWT_COOKIE_NAME + plexpy.CONFIG.PMS_UUID)
|
jwt_cookie = str(JWT_COOKIE_NAME + jellypy.CONFIG.PMS_UUID)
|
||||||
jwt_token = cherrypy.request.cookie.get(jwt_cookie)
|
jwt_token = cherrypy.request.cookie.get(jwt_cookie)
|
||||||
|
|
||||||
if jwt_token:
|
if jwt_token:
|
||||||
try:
|
try:
|
||||||
payload = jwt.decode(
|
payload = jwt.decode(
|
||||||
jwt_token.value, plexpy.CONFIG.JWT_SECRET, leeway=timedelta(seconds=10), algorithms=[JWT_ALGORITHM]
|
jwt_token.value, jellypy.CONFIG.JWT_SECRET, leeway=timedelta(seconds=10), algorithms=[JWT_ALGORITHM]
|
||||||
)
|
)
|
||||||
except (jwt.DecodeError, jwt.ExpiredSignatureError):
|
except (jwt.DecodeError, jwt.ExpiredSignatureError):
|
||||||
return None
|
return None
|
||||||
@@ -186,14 +186,14 @@ def check_auth(*args, **kwargs):
|
|||||||
for condition in conditions:
|
for condition in conditions:
|
||||||
# A condition is just a callable that returns true or false
|
# A condition is just a callable that returns true or false
|
||||||
if not condition():
|
if not condition():
|
||||||
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT)
|
raise cherrypy.HTTPRedirect(jellypy.HTTP_ROOT)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
redirect_uri = cherrypy.request.wsgi_environ['REQUEST_URI']
|
redirect_uri = cherrypy.request.wsgi_environ['REQUEST_URI']
|
||||||
if redirect_uri:
|
if redirect_uri:
|
||||||
redirect_uri = '?redirect_uri=' + quote(redirect_uri)
|
redirect_uri = '?redirect_uri=' + quote(redirect_uri)
|
||||||
|
|
||||||
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT + "auth/logout" + redirect_uri)
|
raise cherrypy.HTTPRedirect(jellypy.HTTP_ROOT + "auth/logout" + redirect_uri)
|
||||||
|
|
||||||
|
|
||||||
def requireAuth(*conditions):
|
def requireAuth(*conditions):
|
||||||
@@ -268,11 +268,11 @@ def check_rate_limit(ip_address):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
last_success = 0
|
last_success = 0
|
||||||
|
|
||||||
max_timestamp = max(last_success, last_timestamp - plexpy.CONFIG.HTTP_RATE_LIMIT_ATTEMPTS_INTERVAL)
|
max_timestamp = max(last_success, last_timestamp - jellypy.CONFIG.HTTP_RATE_LIMIT_ATTEMPTS_INTERVAL)
|
||||||
attempts = [login for login in result if login['timestamp'] >= max_timestamp and not login['success']]
|
attempts = [login for login in result if login['timestamp'] >= max_timestamp and not login['success']]
|
||||||
|
|
||||||
if len(attempts) >= plexpy.CONFIG.HTTP_RATE_LIMIT_ATTEMPTS:
|
if len(attempts) >= jellypy.CONFIG.HTTP_RATE_LIMIT_ATTEMPTS:
|
||||||
return max(last_timestamp - (timestamp() - plexpy.CONFIG.HTTP_RATE_LIMIT_LOCKOUT_TIME), 0)
|
return max(last_timestamp - (timestamp() - jellypy.CONFIG.HTTP_RATE_LIMIT_LOCKOUT_TIME), 0)
|
||||||
|
|
||||||
|
|
||||||
# Controller to provide login and logout actions
|
# Controller to provide login and logout actions
|
||||||
@@ -280,9 +280,9 @@ def check_rate_limit(ip_address):
|
|||||||
class AuthController(object):
|
class AuthController(object):
|
||||||
|
|
||||||
def check_auth_enabled(self):
|
def check_auth_enabled(self):
|
||||||
if not plexpy.CONFIG.HTTP_BASIC_AUTH and plexpy.CONFIG.HTTP_PASSWORD:
|
if not jellypy.CONFIG.HTTP_BASIC_AUTH and jellypy.CONFIG.HTTP_PASSWORD:
|
||||||
return
|
return
|
||||||
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT)
|
raise cherrypy.HTTPRedirect(jellypy.HTTP_ROOT)
|
||||||
|
|
||||||
def on_login(self, username=None, user_id=None, user_group=None, success=False, oauth=False):
|
def on_login(self, username=None, user_id=None, user_group=None, success=False, oauth=False):
|
||||||
"""Called on successful login"""
|
"""Called on successful login"""
|
||||||
@@ -310,12 +310,12 @@ class AuthController(object):
|
|||||||
logger.debug("Tautulli WebAuth :: %s user '%s' logged out of Tautulli." % (user_group.capitalize(), username))
|
logger.debug("Tautulli WebAuth :: %s user '%s' logged out of Tautulli." % (user_group.capitalize(), username))
|
||||||
|
|
||||||
def get_loginform(self, redirect_uri=''):
|
def get_loginform(self, redirect_uri=''):
|
||||||
from plexpy.webserve import serve_template
|
from jellypy.webserve import serve_template
|
||||||
return serve_template(templatename="login.html", title="Login", redirect_uri=unquote(redirect_uri))
|
return serve_template(templatename="login.html", title="Login", redirect_uri=unquote(redirect_uri))
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
def index(self, *args, **kwargs):
|
def index(self, *args, **kwargs):
|
||||||
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT + "auth/login")
|
raise cherrypy.HTTPRedirect(jellypy.HTTP_ROOT + "auth/login")
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
def login(self, redirect_uri='', *args, **kwargs):
|
def login(self, redirect_uri='', *args, **kwargs):
|
||||||
@@ -331,12 +331,12 @@ class AuthController(object):
|
|||||||
if payload:
|
if payload:
|
||||||
self.on_logout(payload['user'], payload['user_group'])
|
self.on_logout(payload['user'], payload['user_group'])
|
||||||
|
|
||||||
jwt_cookie = str(JWT_COOKIE_NAME + plexpy.CONFIG.PMS_UUID)
|
jwt_cookie = str(JWT_COOKIE_NAME + jellypy.CONFIG.PMS_UUID)
|
||||||
cherrypy.response.cookie[jwt_cookie] = ''
|
cherrypy.response.cookie[jwt_cookie] = ''
|
||||||
cherrypy.response.cookie[jwt_cookie]['expires'] = 0
|
cherrypy.response.cookie[jwt_cookie]['expires'] = 0
|
||||||
cherrypy.response.cookie[jwt_cookie]['path'] = plexpy.HTTP_ROOT.rstrip('/') or '/'
|
cherrypy.response.cookie[jwt_cookie]['path'] = jellypy.HTTP_ROOT.rstrip('/') or '/'
|
||||||
|
|
||||||
if plexpy.HTTP_ROOT != '/':
|
if jellypy.HTTP_ROOT != '/':
|
||||||
# Also expire the JWT on the root path
|
# Also expire the JWT on the root path
|
||||||
cherrypy.response.headers['Set-Cookie'] = jwt_cookie + '=""; expires=Thu, 01 Jan 1970 12:00:00 GMT; path=/'
|
cherrypy.response.headers['Set-Cookie'] = jwt_cookie + '=""; expires=Thu, 01 Jan 1970 12:00:00 GMT; path=/'
|
||||||
|
|
||||||
@@ -345,7 +345,7 @@ class AuthController(object):
|
|||||||
if redirect_uri:
|
if redirect_uri:
|
||||||
redirect_uri = '?redirect_uri=' + redirect_uri
|
redirect_uri = '?redirect_uri=' + redirect_uri
|
||||||
|
|
||||||
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT + "auth/login" + redirect_uri)
|
raise cherrypy.HTTPRedirect(jellypy.HTTP_ROOT + "auth/login" + redirect_uri)
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@cherrypy.tools.json_out()
|
@cherrypy.tools.json_out()
|
||||||
@@ -383,7 +383,7 @@ class AuthController(object):
|
|||||||
'exp': expiry
|
'exp': expiry
|
||||||
}
|
}
|
||||||
|
|
||||||
jwt_token = jwt.encode(payload, plexpy.CONFIG.JWT_SECRET, algorithm=JWT_ALGORITHM).decode('utf-8')
|
jwt_token = jwt.encode(payload, jellypy.CONFIG.JWT_SECRET, algorithm=JWT_ALGORITHM).decode('utf-8')
|
||||||
|
|
||||||
self.on_login(username=user_details['username'],
|
self.on_login(username=user_details['username'],
|
||||||
user_id=user_details['user_id'],
|
user_id=user_details['user_id'],
|
||||||
@@ -391,16 +391,16 @@ class AuthController(object):
|
|||||||
success=True,
|
success=True,
|
||||||
oauth=bool(token))
|
oauth=bool(token))
|
||||||
|
|
||||||
jwt_cookie = str(JWT_COOKIE_NAME + plexpy.CONFIG.PMS_UUID)
|
jwt_cookie = str(JWT_COOKIE_NAME + jellypy.CONFIG.PMS_UUID)
|
||||||
cherrypy.response.cookie[jwt_cookie] = jwt_token
|
cherrypy.response.cookie[jwt_cookie] = jwt_token
|
||||||
cherrypy.response.cookie[jwt_cookie]['expires'] = int(time_delta.total_seconds())
|
cherrypy.response.cookie[jwt_cookie]['expires'] = int(time_delta.total_seconds())
|
||||||
cherrypy.response.cookie[jwt_cookie]['path'] = plexpy.HTTP_ROOT.rstrip('/') or '/'
|
cherrypy.response.cookie[jwt_cookie]['path'] = jellypy.HTTP_ROOT.rstrip('/') or '/'
|
||||||
cherrypy.response.cookie[jwt_cookie]['httponly'] = True
|
cherrypy.response.cookie[jwt_cookie]['httponly'] = True
|
||||||
cherrypy.response.cookie[jwt_cookie]['samesite'] = 'lax'
|
cherrypy.response.cookie[jwt_cookie]['samesite'] = 'lax'
|
||||||
|
|
||||||
cherrypy.request.login = payload
|
cherrypy.request.login = payload
|
||||||
cherrypy.response.status = 200
|
cherrypy.response.status = 200
|
||||||
return {'status': 'success', 'token': jwt_token, 'uuid': plexpy.CONFIG.PMS_UUID}
|
return {'status': 'success', 'token': jwt_token, 'uuid': jellypy.CONFIG.PMS_UUID}
|
||||||
|
|
||||||
elif admin_login == '1' and username:
|
elif admin_login == '1' and username:
|
||||||
self.on_login(username=username)
|
self.on_login(username=username)
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -20,34 +20,34 @@ import sys
|
|||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
import logger
|
import logger
|
||||||
import webauth
|
import webauth
|
||||||
from helpers import create_https_certificates
|
from helpers import create_https_certificates
|
||||||
from webserve import WebInterface, BaseRedirect
|
from webserve import WebInterface, BaseRedirect
|
||||||
else:
|
else:
|
||||||
from plexpy import logger
|
from jellypy import logger
|
||||||
from plexpy import webauth
|
from jellypy import webauth
|
||||||
from plexpy.helpers import create_https_certificates
|
from jellypy.helpers import create_https_certificates
|
||||||
from plexpy.webserve import WebInterface, BaseRedirect
|
from jellypy.webserve import WebInterface, BaseRedirect
|
||||||
|
|
||||||
|
|
||||||
def start():
|
def start():
|
||||||
logger.info("Tautulli WebStart :: Initializing Tautulli web server...")
|
logger.info("Tautulli WebStart :: Initializing Tautulli web server...")
|
||||||
web_config = {
|
web_config = {
|
||||||
'http_port': plexpy.HTTP_PORT,
|
'http_port': jellypy.HTTP_PORT,
|
||||||
'http_host': plexpy.CONFIG.HTTP_HOST,
|
'http_host': jellypy.CONFIG.HTTP_HOST,
|
||||||
'http_root': plexpy.CONFIG.HTTP_ROOT,
|
'http_root': jellypy.CONFIG.HTTP_ROOT,
|
||||||
'http_environment': plexpy.CONFIG.HTTP_ENVIRONMENT,
|
'http_environment': jellypy.CONFIG.HTTP_ENVIRONMENT,
|
||||||
'http_proxy': plexpy.CONFIG.HTTP_PROXY,
|
'http_proxy': jellypy.CONFIG.HTTP_PROXY,
|
||||||
'enable_https': plexpy.CONFIG.ENABLE_HTTPS,
|
'enable_https': jellypy.CONFIG.ENABLE_HTTPS,
|
||||||
'https_cert': plexpy.CONFIG.HTTPS_CERT,
|
'https_cert': jellypy.CONFIG.HTTPS_CERT,
|
||||||
'https_cert_chain': plexpy.CONFIG.HTTPS_CERT_CHAIN,
|
'https_cert_chain': jellypy.CONFIG.HTTPS_CERT_CHAIN,
|
||||||
'https_key': plexpy.CONFIG.HTTPS_KEY,
|
'https_key': jellypy.CONFIG.HTTPS_KEY,
|
||||||
'http_username': plexpy.CONFIG.HTTP_USERNAME,
|
'http_username': jellypy.CONFIG.HTTP_USERNAME,
|
||||||
'http_password': plexpy.CONFIG.HTTP_PASSWORD,
|
'http_password': jellypy.CONFIG.HTTP_PASSWORD,
|
||||||
'http_basic_auth': plexpy.CONFIG.HTTP_BASIC_AUTH
|
'http_basic_auth': jellypy.CONFIG.HTTP_BASIC_AUTH
|
||||||
}
|
}
|
||||||
initialize(web_config)
|
initialize(web_config)
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ def initialize(options):
|
|||||||
|
|
||||||
if enable_https:
|
if enable_https:
|
||||||
# If either the HTTPS certificate or key do not exist, try to make self-signed ones.
|
# If either the HTTPS certificate or key do not exist, try to make self-signed ones.
|
||||||
if plexpy.CONFIG.HTTPS_CREATE_CERT and \
|
if jellypy.CONFIG.HTTPS_CREATE_CERT and \
|
||||||
(not (https_cert and os.path.exists(https_cert)) or
|
(not (https_cert and os.path.exists(https_cert)) or
|
||||||
not (https_key and os.path.exists(https_key))):
|
not (https_key and os.path.exists(https_key))):
|
||||||
if not create_https_certificates(https_cert, https_key):
|
if not create_https_certificates(https_cert, https_key):
|
||||||
@@ -96,7 +96,7 @@ def initialize(options):
|
|||||||
'tools.decode.on': True
|
'tools.decode.on': True
|
||||||
}
|
}
|
||||||
|
|
||||||
if plexpy.DEV:
|
if jellypy.DEV:
|
||||||
options_dict['environment'] = "test_suite"
|
options_dict['environment'] = "test_suite"
|
||||||
options_dict['engine.autoreload.on'] = True
|
options_dict['engine.autoreload.on'] = True
|
||||||
|
|
||||||
@@ -114,39 +114,39 @@ def initialize(options):
|
|||||||
|
|
||||||
if options['http_password']:
|
if options['http_password']:
|
||||||
login_allowed = ["Tautulli admin (username is '%s')" % options['http_username']]
|
login_allowed = ["Tautulli admin (username is '%s')" % options['http_username']]
|
||||||
if plexpy.CONFIG.HTTP_PLEX_ADMIN:
|
if jellypy.CONFIG.HTTP_PLEX_ADMIN:
|
||||||
login_allowed.append("Plex admin")
|
login_allowed.append("Plex admin")
|
||||||
|
|
||||||
logger.info("Tautulli WebStart :: Web server authentication is enabled: %s.", ' and '.join(login_allowed))
|
logger.info("Tautulli WebStart :: Web server authentication is enabled: %s.", ' and '.join(login_allowed))
|
||||||
|
|
||||||
if options['http_basic_auth']:
|
if options['http_basic_auth']:
|
||||||
plexpy.AUTH_ENABLED = False
|
jellypy.AUTH_ENABLED = False
|
||||||
basic_auth_enabled = True
|
basic_auth_enabled = True
|
||||||
else:
|
else:
|
||||||
plexpy.AUTH_ENABLED = True
|
jellypy.AUTH_ENABLED = True
|
||||||
basic_auth_enabled = False
|
basic_auth_enabled = False
|
||||||
cherrypy.tools.auth = cherrypy.Tool('before_handler', webauth.check_auth, priority=2)
|
cherrypy.tools.auth = cherrypy.Tool('before_handler', webauth.check_auth, priority=2)
|
||||||
else:
|
else:
|
||||||
plexpy.AUTH_ENABLED = False
|
jellypy.AUTH_ENABLED = False
|
||||||
basic_auth_enabled = False
|
basic_auth_enabled = False
|
||||||
|
|
||||||
if options['http_root'].strip('/'):
|
if options['http_root'].strip('/'):
|
||||||
plexpy.HTTP_ROOT = options['http_root'] = '/' + str(options['http_root'].strip('/')) + '/'
|
jellypy.HTTP_ROOT = options['http_root'] = '/' + str(options['http_root'].strip('/')) + '/'
|
||||||
else:
|
else:
|
||||||
plexpy.HTTP_ROOT = options['http_root'] = '/'
|
jellypy.HTTP_ROOT = options['http_root'] = '/'
|
||||||
|
|
||||||
cherrypy.config.update(options_dict)
|
cherrypy.config.update(options_dict)
|
||||||
|
|
||||||
conf = {
|
conf = {
|
||||||
'/': {
|
'/': {
|
||||||
'engine.timeout_monitor.on': False,
|
'engine.timeout_monitor.on': False,
|
||||||
'tools.staticdir.root': os.path.join(plexpy.PROG_DIR, 'data'),
|
'tools.staticdir.root': os.path.join(jellypy.PROG_DIR, 'data'),
|
||||||
'tools.proxy.on': bool(options['http_proxy']),
|
'tools.proxy.on': bool(options['http_proxy']),
|
||||||
'tools.gzip.on': True,
|
'tools.gzip.on': True,
|
||||||
'tools.gzip.mime_types': ['text/html', 'text/plain', 'text/css',
|
'tools.gzip.mime_types': ['text/html', 'text/plain', 'text/css',
|
||||||
'text/javascript', 'application/json',
|
'text/javascript', 'application/json',
|
||||||
'application/javascript'],
|
'application/javascript'],
|
||||||
'tools.auth.on': plexpy.AUTH_ENABLED,
|
'tools.auth.on': jellypy.AUTH_ENABLED,
|
||||||
'tools.auth_basic.on': basic_auth_enabled,
|
'tools.auth_basic.on': basic_auth_enabled,
|
||||||
'tools.auth_basic.realm': 'Tautulli web server',
|
'tools.auth_basic.realm': 'Tautulli web server',
|
||||||
'tools.auth_basic.checkpassword': cherrypy.lib.auth_basic.checkpassword_dict({
|
'tools.auth_basic.checkpassword': cherrypy.lib.auth_basic.checkpassword_dict({
|
||||||
@@ -216,7 +216,7 @@ def initialize(options):
|
|||||||
},
|
},
|
||||||
'/cache': {
|
'/cache': {
|
||||||
'tools.staticdir.on': True,
|
'tools.staticdir.on': True,
|
||||||
'tools.staticdir.dir': plexpy.CONFIG.CACHE_DIR,
|
'tools.staticdir.dir': jellypy.CONFIG.CACHE_DIR,
|
||||||
'tools.caching.on': True,
|
'tools.caching.on': True,
|
||||||
'tools.caching.force': True,
|
'tools.caching.force': True,
|
||||||
'tools.caching.delay': 0,
|
'tools.caching.delay': 0,
|
||||||
@@ -227,7 +227,7 @@ def initialize(options):
|
|||||||
},
|
},
|
||||||
#'/pms_image_proxy': {
|
#'/pms_image_proxy': {
|
||||||
# 'tools.staticdir.on': True,
|
# 'tools.staticdir.on': True,
|
||||||
# 'tools.staticdir.dir': os.path.join(plexpy.CONFIG.CACHE_DIR, 'images'),
|
# 'tools.staticdir.dir': os.path.join(jellypy.CONFIG.CACHE_DIR, 'images'),
|
||||||
# 'tools.caching.on': True,
|
# 'tools.caching.on': True,
|
||||||
# 'tools.caching.force': True,
|
# 'tools.caching.force': True,
|
||||||
# 'tools.caching.delay': 0,
|
# 'tools.caching.delay': 0,
|
||||||
@@ -238,7 +238,7 @@ def initialize(options):
|
|||||||
#},
|
#},
|
||||||
'/favicon.ico': {
|
'/favicon.ico': {
|
||||||
'tools.staticfile.on': True,
|
'tools.staticfile.on': True,
|
||||||
'tools.staticfile.filename': os.path.abspath(os.path.join(plexpy.PROG_DIR, 'data/interfaces/default/images/favicon/favicon.ico')),
|
'tools.staticfile.filename': os.path.abspath(os.path.join(jellypy.PROG_DIR, 'data/interfaces/default/images/favicon/favicon.ico')),
|
||||||
'tools.caching.on': True,
|
'tools.caching.on': True,
|
||||||
'tools.caching.force': True,
|
'tools.caching.force': True,
|
||||||
'tools.caching.delay': 0,
|
'tools.caching.delay': 0,
|
||||||
@@ -250,14 +250,14 @@ def initialize(options):
|
|||||||
}
|
}
|
||||||
|
|
||||||
cherrypy.tree.mount(WebInterface(), options['http_root'], config=conf)
|
cherrypy.tree.mount(WebInterface(), options['http_root'], config=conf)
|
||||||
if plexpy.HTTP_ROOT != '/':
|
if jellypy.HTTP_ROOT != '/':
|
||||||
cherrypy.tree.mount(BaseRedirect(), '/')
|
cherrypy.tree.mount(BaseRedirect(), '/')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logger.info("Tautulli WebStart :: Starting Tautulli web server on %s://%s:%d%s", protocol,
|
logger.info("Tautulli WebStart :: Starting Tautulli web server on %s://%s:%d%s", protocol,
|
||||||
options['http_host'], options['http_port'], options['http_root'])
|
options['http_host'], options['http_port'], options['http_root'])
|
||||||
#cherrypy.process.servers.check_port(str(options['http_host']), options['http_port'])
|
#cherrypy.process.servers.check_port(str(options['http_host']), options['http_port'])
|
||||||
if not plexpy.DEV:
|
if not jellypy.DEV:
|
||||||
cherrypy.server.start()
|
cherrypy.server.start()
|
||||||
else:
|
else:
|
||||||
cherrypy.engine.signals.subscribe()
|
cherrypy.engine.signals.subscribe()
|
||||||
@@ -29,34 +29,34 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
import _winreg as winreg
|
import _winreg as winreg
|
||||||
|
|
||||||
import plexpy
|
import jellypy
|
||||||
if plexpy.PYTHON2:
|
if jellypy.PYTHON2:
|
||||||
import common
|
import common
|
||||||
import logger
|
import logger
|
||||||
import versioncheck
|
import versioncheck
|
||||||
else:
|
else:
|
||||||
from plexpy import common
|
from jellypy import common
|
||||||
from plexpy import logger
|
from jellypy import logger
|
||||||
from plexpy import versioncheck
|
from jellypy import versioncheck
|
||||||
|
|
||||||
|
|
||||||
class WindowsSystemTray(object):
|
class WindowsSystemTray(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.image_dir = os.path.join(plexpy.PROG_DIR, 'data/interfaces/', plexpy.CONFIG.INTERFACE, 'images')
|
self.image_dir = os.path.join(jellypy.PROG_DIR, 'data/interfaces/', jellypy.CONFIG.INTERFACE, 'images')
|
||||||
self.icon = os.path.join(self.image_dir, 'logo-circle.ico')
|
self.icon = os.path.join(self.image_dir, 'logo-circle.ico')
|
||||||
|
|
||||||
if plexpy.UPDATE_AVAILABLE:
|
if jellypy.UPDATE_AVAILABLE:
|
||||||
self.hover_text = common.PRODUCT + ' - Update Available!'
|
self.hover_text = common.PRODUCT + ' - Update Available!'
|
||||||
self.update_title = 'Check for Updates - Update Available!'
|
self.update_title = 'Check for Updates - Update Available!'
|
||||||
else:
|
else:
|
||||||
self.hover_text = common.PRODUCT
|
self.hover_text = common.PRODUCT
|
||||||
self.update_title = 'Check for Updates'
|
self.update_title = 'Check for Updates'
|
||||||
|
|
||||||
if plexpy.CONFIG.LAUNCH_STARTUP:
|
if jellypy.CONFIG.LAUNCH_STARTUP:
|
||||||
launch_start_icon = os.path.join(self.image_dir, 'check-solid.ico')
|
launch_start_icon = os.path.join(self.image_dir, 'check-solid.ico')
|
||||||
else:
|
else:
|
||||||
launch_start_icon = None
|
launch_start_icon = None
|
||||||
if plexpy.CONFIG.LAUNCH_BROWSER:
|
if jellypy.CONFIG.LAUNCH_BROWSER:
|
||||||
launch_browser_icon = os.path.join(self.image_dir, 'check-solid.ico')
|
launch_browser_icon = os.path.join(self.image_dir, 'check-solid.ico')
|
||||||
else:
|
else:
|
||||||
launch_browser_icon = None
|
launch_browser_icon = None
|
||||||
@@ -70,7 +70,7 @@ class WindowsSystemTray(object):
|
|||||||
[self.update_title, None, self.tray_check_update, None],
|
[self.update_title, None, self.tray_check_update, None],
|
||||||
['Restart', None, self.tray_restart, None]
|
['Restart', None, self.tray_restart, None]
|
||||||
]
|
]
|
||||||
if not plexpy.FROZEN:
|
if not jellypy.FROZEN:
|
||||||
self.menu.insert(6, ['Update', None, self.tray_update, None])
|
self.menu.insert(6, ['Update', None, self.tray_update, None])
|
||||||
|
|
||||||
self.tray_icon = SysTrayIcon(self.icon, self.hover_text, self.menu, on_quit=self.tray_quit)
|
self.tray_icon = SysTrayIcon(self.icon, self.hover_text, self.menu, on_quit=self.tray_quit)
|
||||||
@@ -89,22 +89,22 @@ class WindowsSystemTray(object):
|
|||||||
self.tray_icon.update(**kwargs)
|
self.tray_icon.update(**kwargs)
|
||||||
|
|
||||||
def tray_open(self, tray_icon):
|
def tray_open(self, tray_icon):
|
||||||
plexpy.launch_browser(plexpy.CONFIG.HTTP_HOST, plexpy.HTTP_PORT, plexpy.HTTP_ROOT)
|
jellypy.launch_browser(jellypy.CONFIG.HTTP_HOST, jellypy.HTTP_PORT, jellypy.HTTP_ROOT)
|
||||||
|
|
||||||
def tray_startup(self, tray_icon):
|
def tray_startup(self, tray_icon):
|
||||||
plexpy.CONFIG.LAUNCH_STARTUP = not plexpy.CONFIG.LAUNCH_STARTUP
|
jellypy.CONFIG.LAUNCH_STARTUP = not jellypy.CONFIG.LAUNCH_STARTUP
|
||||||
set_startup()
|
set_startup()
|
||||||
|
|
||||||
def tray_browser(self, tray_icon):
|
def tray_browser(self, tray_icon):
|
||||||
plexpy.CONFIG.LAUNCH_BROWSER = not plexpy.CONFIG.LAUNCH_BROWSER
|
jellypy.CONFIG.LAUNCH_BROWSER = not jellypy.CONFIG.LAUNCH_BROWSER
|
||||||
set_startup()
|
set_startup()
|
||||||
|
|
||||||
def tray_check_update(self, tray_icon):
|
def tray_check_update(self, tray_icon):
|
||||||
versioncheck.check_update()
|
versioncheck.check_update()
|
||||||
|
|
||||||
def tray_update(self, tray_icon):
|
def tray_update(self, tray_icon):
|
||||||
if plexpy.UPDATE_AVAILABLE:
|
if jellypy.UPDATE_AVAILABLE:
|
||||||
plexpy.SIGNAL = 'update'
|
jellypy.SIGNAL = 'update'
|
||||||
else:
|
else:
|
||||||
self.hover_text = common.PRODUCT + ' - No Update Available'
|
self.hover_text = common.PRODUCT + ' - No Update Available'
|
||||||
self.update_title = 'Check for Updates - No Update Available'
|
self.update_title = 'Check for Updates - No Update Available'
|
||||||
@@ -112,13 +112,13 @@ class WindowsSystemTray(object):
|
|||||||
self.update(hover_text=self.hover_text, menu_options=self.menu)
|
self.update(hover_text=self.hover_text, menu_options=self.menu)
|
||||||
|
|
||||||
def tray_restart(self, tray_icon):
|
def tray_restart(self, tray_icon):
|
||||||
plexpy.SIGNAL = 'restart'
|
jellypy.SIGNAL = 'restart'
|
||||||
|
|
||||||
def tray_quit(self, tray_icon):
|
def tray_quit(self, tray_icon):
|
||||||
plexpy.SIGNAL = 'shutdown'
|
jellypy.SIGNAL = 'shutdown'
|
||||||
|
|
||||||
def change_tray_update_icon(self):
|
def change_tray_update_icon(self):
|
||||||
if plexpy.UPDATE_AVAILABLE:
|
if jellypy.UPDATE_AVAILABLE:
|
||||||
self.hover_text = common.PRODUCT + ' - Update Available!'
|
self.hover_text = common.PRODUCT + ' - Update Available!'
|
||||||
self.update_title = 'Check for Updates - Update Available!'
|
self.update_title = 'Check for Updates - Update Available!'
|
||||||
else:
|
else:
|
||||||
@@ -128,11 +128,11 @@ class WindowsSystemTray(object):
|
|||||||
self.update(hover_text=self.hover_text, menu_options=self.menu)
|
self.update(hover_text=self.hover_text, menu_options=self.menu)
|
||||||
|
|
||||||
def change_tray_icons(self):
|
def change_tray_icons(self):
|
||||||
if plexpy.CONFIG.LAUNCH_STARTUP:
|
if jellypy.CONFIG.LAUNCH_STARTUP:
|
||||||
launch_start_icon = os.path.join(self.image_dir, 'check-solid.ico')
|
launch_start_icon = os.path.join(self.image_dir, 'check-solid.ico')
|
||||||
else:
|
else:
|
||||||
launch_start_icon = None
|
launch_start_icon = None
|
||||||
if plexpy.CONFIG.LAUNCH_BROWSER:
|
if jellypy.CONFIG.LAUNCH_BROWSER:
|
||||||
launch_browser_icon = os.path.join(self.image_dir, 'check-solid.ico')
|
launch_browser_icon = os.path.join(self.image_dir, 'check-solid.ico')
|
||||||
else:
|
else:
|
||||||
launch_browser_icon = None
|
launch_browser_icon = None
|
||||||
@@ -142,23 +142,23 @@ class WindowsSystemTray(object):
|
|||||||
|
|
||||||
|
|
||||||
def set_startup():
|
def set_startup():
|
||||||
if plexpy.WIN_SYS_TRAY_ICON:
|
if jellypy.WIN_SYS_TRAY_ICON:
|
||||||
plexpy.WIN_SYS_TRAY_ICON.change_tray_icons()
|
jellypy.WIN_SYS_TRAY_ICON.change_tray_icons()
|
||||||
|
|
||||||
startup_reg_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Run"
|
startup_reg_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Run"
|
||||||
|
|
||||||
exe = sys.executable
|
exe = sys.executable
|
||||||
run_args = [arg for arg in plexpy.ARGS if arg != '--nolaunch']
|
run_args = [arg for arg in jellypy.ARGS if arg != '--nolaunch']
|
||||||
if plexpy.FROZEN:
|
if jellypy.FROZEN:
|
||||||
args = [exe] + run_args
|
args = [exe] + run_args
|
||||||
else:
|
else:
|
||||||
args = [exe, plexpy.FULL_PATH] + run_args
|
args = [exe, jellypy.FULL_PATH] + run_args
|
||||||
|
|
||||||
registry_key_name = '{}_{}'.format(common.PRODUCT, plexpy.CONFIG.PMS_UUID)
|
registry_key_name = '{}_{}'.format(common.PRODUCT, jellypy.CONFIG.PMS_UUID)
|
||||||
|
|
||||||
cmd = ' '.join(cmd_quote(arg) for arg in args).replace('python.exe', 'pythonw.exe').replace("'", '"')
|
cmd = ' '.join(cmd_quote(arg) for arg in args).replace('python.exe', 'pythonw.exe').replace("'", '"')
|
||||||
|
|
||||||
if plexpy.CONFIG.LAUNCH_STARTUP:
|
if jellypy.CONFIG.LAUNCH_STARTUP:
|
||||||
# Rename old Tautulli registry key
|
# Rename old Tautulli registry key
|
||||||
try:
|
try:
|
||||||
registry_key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, startup_reg_path, 0, winreg.KEY_ALL_ACCESS)
|
registry_key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, startup_reg_path, 0, winreg.KEY_ALL_ACCESS)
|
||||||
1652
lib/IPy.py
1652
lib/IPy.py
File diff suppressed because it is too large
Load Diff
@@ -1,121 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
###############################################################################
|
|
||||||
# Formatting filter for urllib2's HTTPHandler(debuglevel=1) output
|
|
||||||
# Copyright (c) 2013, Analytics Pros
|
|
||||||
#
|
|
||||||
# This project is free software, distributed under the BSD license.
|
|
||||||
# Analytics Pros offers consulting and integration services if your firm needs
|
|
||||||
# assistance in strategy, implementation, or auditing existing work.
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
|
|
||||||
import sys, re, os
|
|
||||||
from io import StringIO
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class BufferTranslator(object):
|
|
||||||
""" Provides a buffer-compatible interface for filtering buffer content.
|
|
||||||
"""
|
|
||||||
parsers = []
|
|
||||||
|
|
||||||
def __init__(self, output):
|
|
||||||
self.output = output
|
|
||||||
self.encoding = getattr(output, 'encoding', None)
|
|
||||||
|
|
||||||
def write(self, content):
|
|
||||||
content = self.translate(content)
|
|
||||||
self.output.write(content)
|
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def stripslashes(content):
|
|
||||||
return content.decode('string_escape')
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def addslashes(content):
|
|
||||||
return content.encode('string_escape')
|
|
||||||
|
|
||||||
def translate(self, line):
|
|
||||||
for pattern, method in self.parsers:
|
|
||||||
match = pattern.match(line)
|
|
||||||
if match:
|
|
||||||
return method(match)
|
|
||||||
|
|
||||||
return line
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class LineBufferTranslator(BufferTranslator):
|
|
||||||
""" Line buffer implementation supports translation of line-format input
|
|
||||||
even when input is not already line-buffered. Caches input until newlines
|
|
||||||
occur, and then dispatches translated input to output buffer.
|
|
||||||
"""
|
|
||||||
def __init__(self, *a, **kw):
|
|
||||||
self._linepending = []
|
|
||||||
super(LineBufferTranslator, self).__init__(*a, **kw)
|
|
||||||
|
|
||||||
def write(self, _input):
|
|
||||||
lines = _input.splitlines(True)
|
|
||||||
for i in range(0, len(lines)):
|
|
||||||
last = i
|
|
||||||
if lines[i].endswith('\n'):
|
|
||||||
prefix = len(self._linepending) and ''.join(self._linepending) or ''
|
|
||||||
self.output.write(self.translate(prefix + lines[i]))
|
|
||||||
del self._linepending[0:]
|
|
||||||
last = -1
|
|
||||||
|
|
||||||
if last >= 0:
|
|
||||||
self._linepending.append(lines[ last ])
|
|
||||||
|
|
||||||
|
|
||||||
def __del__(self):
|
|
||||||
if len(self._linepending):
|
|
||||||
self.output.write(self.translate(''.join(self._linepending)))
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPTranslator(LineBufferTranslator):
|
|
||||||
""" Translates output from |urllib2| HTTPHandler(debuglevel = 1) into
|
|
||||||
HTTP-compatible, readible text structures for human analysis.
|
|
||||||
"""
|
|
||||||
|
|
||||||
RE_LINE_PARSER = re.compile(r'^(?:([a-z]+):)\s*(\'?)([^\r\n]*)\2(?:[\r\n]*)$')
|
|
||||||
RE_LINE_BREAK = re.compile(r'(\r?\n|(?:\\r)?\\n)')
|
|
||||||
RE_HTTP_METHOD = re.compile(r'^(POST|GET|HEAD|DELETE|PUT|TRACE|OPTIONS)')
|
|
||||||
RE_PARAMETER_SPACER = re.compile(r'&([a-z0-9]+)=')
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def spacer(cls, line):
|
|
||||||
return cls.RE_PARAMETER_SPACER.sub(r' &\1= ', line)
|
|
||||||
|
|
||||||
def translate(self, line):
|
|
||||||
|
|
||||||
parsed = self.RE_LINE_PARSER.match(line)
|
|
||||||
|
|
||||||
if parsed:
|
|
||||||
value = parsed.group(3)
|
|
||||||
stage = parsed.group(1)
|
|
||||||
|
|
||||||
if stage == 'send': # query string is rendered here
|
|
||||||
return '\n# HTTP Request:\n' + self.stripslashes(value)
|
|
||||||
elif stage == 'reply':
|
|
||||||
return '\n\n# HTTP Response:\n' + self.stripslashes(value)
|
|
||||||
elif stage == 'header':
|
|
||||||
return value + '\n'
|
|
||||||
else:
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
return line
|
|
||||||
|
|
||||||
|
|
||||||
def consume(outbuffer = None): # Capture standard output
|
|
||||||
sys.stdout = HTTPTranslator(outbuffer or sys.stdout)
|
|
||||||
return sys.stdout
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
consume(sys.stdout).write(sys.stdin.read())
|
|
||||||
print('\n')
|
|
||||||
|
|
||||||
# vim: set nowrap tabstop=4 shiftwidth=4 softtabstop=0 expandtab textwidth=0 filetype=python foldmethod=indent foldcolumn=4
|
|
||||||
@@ -1,424 +0,0 @@
|
|||||||
from future.moves.urllib.request import urlopen, build_opener, install_opener
|
|
||||||
from future.moves.urllib.request import Request, HTTPSHandler
|
|
||||||
from future.moves.urllib.error import URLError, HTTPError
|
|
||||||
from future.moves.urllib.parse import urlencode
|
|
||||||
|
|
||||||
import random
|
|
||||||
import datetime
|
|
||||||
import time
|
|
||||||
import uuid
|
|
||||||
import hashlib
|
|
||||||
import socket
|
|
||||||
|
|
||||||
|
|
||||||
def generate_uuid(basedata=None):
|
|
||||||
""" Provides a _random_ UUID with no input, or a UUID4-format MD5 checksum of any input data provided """
|
|
||||||
if basedata is None:
|
|
||||||
return str(uuid.uuid4())
|
|
||||||
elif isinstance(basedata, str):
|
|
||||||
checksum = hashlib.md5(str(basedata).encode('utf-8')).hexdigest()
|
|
||||||
return '%8s-%4s-%4s-%4s-%12s' % (
|
|
||||||
checksum[0:8], checksum[8:12], checksum[12:16], checksum[16:20], checksum[20:32])
|
|
||||||
|
|
||||||
|
|
||||||
class Time(datetime.datetime):
|
|
||||||
""" Wrappers and convenience methods for processing various time representations """
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_unix(cls, seconds, milliseconds=0):
|
|
||||||
""" Produce a full |datetime.datetime| object from a Unix timestamp """
|
|
||||||
base = list(time.gmtime(seconds))[0:6]
|
|
||||||
base.append(milliseconds * 1000) # microseconds
|
|
||||||
return cls(*base)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def to_unix(cls, timestamp):
|
|
||||||
""" Wrapper over time module to produce Unix epoch time as a float """
|
|
||||||
if not isinstance(timestamp, datetime.datetime):
|
|
||||||
raise TypeError('Time.milliseconds expects a datetime object')
|
|
||||||
base = time.mktime(timestamp.timetuple())
|
|
||||||
return base
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def milliseconds_offset(cls, timestamp, now=None):
|
|
||||||
""" Offset time (in milliseconds) from a |datetime.datetime| object to now """
|
|
||||||
if isinstance(timestamp, (int, float)):
|
|
||||||
base = timestamp
|
|
||||||
else:
|
|
||||||
base = cls.to_unix(timestamp)
|
|
||||||
base = base + (timestamp.microsecond / 1000000)
|
|
||||||
if now is None:
|
|
||||||
now = time.time()
|
|
||||||
return (now - base) * 1000
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPRequest(object):
|
|
||||||
""" URL Construction and request handling abstraction.
|
|
||||||
This is not intended to be used outside this module.
|
|
||||||
|
|
||||||
Automates mapping of persistent state (i.e. query parameters)
|
|
||||||
onto transcient datasets for each query.
|
|
||||||
"""
|
|
||||||
|
|
||||||
endpoint = 'https://www.google-analytics.com/collect'
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def debug():
|
|
||||||
""" Activate debugging on urllib2 """
|
|
||||||
handler = HTTPSHandler(debuglevel=1)
|
|
||||||
opener = build_opener(handler)
|
|
||||||
install_opener(opener)
|
|
||||||
|
|
||||||
# Store properties for all requests
|
|
||||||
def __init__(self, user_agent=None, *args, **opts):
|
|
||||||
self.user_agent = user_agent or 'Analytics Pros - Universal Analytics (Python)'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def fixUTF8(cls, data): # Ensure proper encoding for UA's servers...
|
|
||||||
""" Convert all strings to UTF-8 """
|
|
||||||
for key in data:
|
|
||||||
if isinstance(data[key], str):
|
|
||||||
data[key] = data[key].encode('utf-8')
|
|
||||||
return data
|
|
||||||
|
|
||||||
# Apply stored properties to the given dataset & POST to the configured endpoint
|
|
||||||
def send(self, data):
|
|
||||||
request = Request(
|
|
||||||
self.endpoint + '?' + urlencode(self.fixUTF8(data)).encode('utf-8'),
|
|
||||||
headers={
|
|
||||||
'User-Agent': self.user_agent
|
|
||||||
}
|
|
||||||
)
|
|
||||||
self.open(request)
|
|
||||||
|
|
||||||
def open(self, request):
|
|
||||||
try:
|
|
||||||
return urlopen(request)
|
|
||||||
except HTTPError as e:
|
|
||||||
return False
|
|
||||||
except URLError as e:
|
|
||||||
self.cache_request(request)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def cache_request(self, request):
|
|
||||||
# TODO: implement a proper caching mechanism here for re-transmitting hits
|
|
||||||
# record = (Time.now(), request.get_full_url(), request.get_data(), request.headers)
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPPost(HTTPRequest):
|
|
||||||
|
|
||||||
# Apply stored properties to the given dataset & POST to the configured endpoint
|
|
||||||
def send(self, data):
|
|
||||||
request = Request(
|
|
||||||
self.endpoint,
|
|
||||||
data=urlencode(self.fixUTF8(data)).encode('utf-8'),
|
|
||||||
headers={
|
|
||||||
'User-Agent': self.user_agent
|
|
||||||
}
|
|
||||||
)
|
|
||||||
self.open(request)
|
|
||||||
|
|
||||||
|
|
||||||
class Tracker(object):
|
|
||||||
""" Primary tracking interface for Universal Analytics """
|
|
||||||
params = None
|
|
||||||
parameter_alias = {}
|
|
||||||
valid_hittypes = ('pageview', 'event', 'social', 'screenview', 'transaction', 'item', 'exception', 'timing')
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def alias(cls, typemap, base, *names):
|
|
||||||
""" Declare an alternate (humane) name for a measurement protocol parameter """
|
|
||||||
cls.parameter_alias[base] = (typemap, base)
|
|
||||||
for i in names:
|
|
||||||
cls.parameter_alias[i] = (typemap, base)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def coerceParameter(cls, name, value=None):
|
|
||||||
if isinstance(name, str) and name[0] == '&':
|
|
||||||
return name[1:], str(value)
|
|
||||||
elif name in cls.parameter_alias:
|
|
||||||
typecast, param_name = cls.parameter_alias.get(name)
|
|
||||||
return param_name, typecast(value)
|
|
||||||
else:
|
|
||||||
raise KeyError('Parameter "{0}" is not recognized'.format(name))
|
|
||||||
|
|
||||||
def payload(self, data):
|
|
||||||
for key, value in data.items():
|
|
||||||
try:
|
|
||||||
yield self.coerceParameter(key, value)
|
|
||||||
except KeyError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
option_sequence = {
|
|
||||||
'pageview': [(str, 'dp')],
|
|
||||||
'event': [(str, 'ec'), (str, 'ea'), (str, 'el'), (int, 'ev')],
|
|
||||||
'social': [(str, 'sn'), (str, 'sa'), (str, 'st')],
|
|
||||||
'timing': [(str, 'utc'), (str, 'utv'), (str, 'utt'), (str, 'utl')]
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def consume_options(cls, data, hittype, args):
|
|
||||||
""" Interpret sequential arguments related to known hittypes based on declared structures """
|
|
||||||
opt_position = 0
|
|
||||||
data['t'] = hittype # integrate hit type parameter
|
|
||||||
if hittype in cls.option_sequence:
|
|
||||||
for expected_type, optname in cls.option_sequence[hittype]:
|
|
||||||
if opt_position < len(args) and isinstance(args[opt_position], expected_type):
|
|
||||||
data[optname] = args[opt_position]
|
|
||||||
opt_position += 1
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def hittime(cls, timestamp=None, age=None, milliseconds=None):
|
|
||||||
""" Returns an integer represeting the milliseconds offset for a given hit (relative to now) """
|
|
||||||
if isinstance(timestamp, (int, float)):
|
|
||||||
return int(Time.milliseconds_offset(Time.from_unix(timestamp, milliseconds=milliseconds)))
|
|
||||||
if isinstance(timestamp, datetime.datetime):
|
|
||||||
return int(Time.milliseconds_offset(timestamp))
|
|
||||||
if isinstance(age, (int, float)):
|
|
||||||
return int(age * 1000) + (milliseconds or 0)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def account(self):
|
|
||||||
return self.params.get('tid', None)
|
|
||||||
|
|
||||||
def __init__(self, account, name=None, client_id=None, hash_client_id=False, user_id=None, user_agent=None,
|
|
||||||
use_post=True):
|
|
||||||
|
|
||||||
if use_post is False:
|
|
||||||
self.http = HTTPRequest(user_agent=user_agent)
|
|
||||||
else:
|
|
||||||
self.http = HTTPPost(user_agent=user_agent)
|
|
||||||
|
|
||||||
self.params = {'v': 1, 'tid': account}
|
|
||||||
|
|
||||||
if client_id is None:
|
|
||||||
client_id = generate_uuid()
|
|
||||||
|
|
||||||
self.params['cid'] = client_id
|
|
||||||
|
|
||||||
self.hash_client_id = hash_client_id
|
|
||||||
|
|
||||||
if user_id is not None:
|
|
||||||
self.params['uid'] = user_id
|
|
||||||
|
|
||||||
def set_timestamp(self, data):
|
|
||||||
""" Interpret time-related options, apply queue-time parameter as needed """
|
|
||||||
if 'hittime' in data: # an absolute timestamp
|
|
||||||
data['qt'] = self.hittime(timestamp=data.pop('hittime', None))
|
|
||||||
if 'hitage' in data: # a relative age (in seconds)
|
|
||||||
data['qt'] = self.hittime(age=data.pop('hitage', None))
|
|
||||||
|
|
||||||
def send(self, hittype, *args, **data):
|
|
||||||
""" Transmit HTTP requests to Google Analytics using the measurement protocol """
|
|
||||||
|
|
||||||
if hittype not in self.valid_hittypes:
|
|
||||||
raise KeyError('Unsupported Universal Analytics Hit Type: {0}'.format(repr(hittype)))
|
|
||||||
|
|
||||||
self.set_timestamp(data)
|
|
||||||
self.consume_options(data, hittype, args)
|
|
||||||
|
|
||||||
for item in args: # process dictionary-object arguments of transcient data
|
|
||||||
if isinstance(item, dict):
|
|
||||||
for key, val in self.payload(item):
|
|
||||||
data[key] = val
|
|
||||||
|
|
||||||
for k, v in self.params.items(): # update only absent parameters
|
|
||||||
if k not in data:
|
|
||||||
data[k] = v
|
|
||||||
|
|
||||||
data = dict(self.payload(data))
|
|
||||||
|
|
||||||
if self.hash_client_id:
|
|
||||||
data['cid'] = generate_uuid(data['cid'])
|
|
||||||
|
|
||||||
# Transmit the hit to Google...
|
|
||||||
self.http.send(data)
|
|
||||||
|
|
||||||
# Setting persistent attibutes of the session/hit/etc (inc. custom dimensions/metrics)
|
|
||||||
def set(self, name, value=None):
|
|
||||||
if isinstance(name, dict):
|
|
||||||
for key, value in name.items():
|
|
||||||
try:
|
|
||||||
param, value = self.coerceParameter(key, value)
|
|
||||||
self.params[param] = value
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
elif isinstance(name, str):
|
|
||||||
try:
|
|
||||||
param, value = self.coerceParameter(name, value)
|
|
||||||
self.params[param] = value
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def __getitem__(self, name):
|
|
||||||
param, value = self.coerceParameter(name, None)
|
|
||||||
return self.params.get(param, None)
|
|
||||||
|
|
||||||
def __setitem__(self, name, value):
|
|
||||||
param, value = self.coerceParameter(name, value)
|
|
||||||
self.params[param] = value
|
|
||||||
|
|
||||||
def __delitem__(self, name):
|
|
||||||
param, value = self.coerceParameter(name, None)
|
|
||||||
if param in self.params:
|
|
||||||
del self.params[param]
|
|
||||||
|
|
||||||
|
|
||||||
def safe_unicode(obj):
|
|
||||||
""" Safe convertion to the Unicode string version of the object """
|
|
||||||
try:
|
|
||||||
return str(obj)
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
return obj.decode('utf-8')
|
|
||||||
|
|
||||||
|
|
||||||
# Declaring name mappings for Measurement Protocol parameters
|
|
||||||
MAX_CUSTOM_DEFINITIONS = 200
|
|
||||||
MAX_EC_LISTS = 11 # 1-based index
|
|
||||||
MAX_EC_PRODUCTS = 11 # 1-based index
|
|
||||||
MAX_EC_PROMOTIONS = 11 # 1-based index
|
|
||||||
|
|
||||||
Tracker.alias(int, 'v', 'protocol-version')
|
|
||||||
Tracker.alias(safe_unicode, 'cid', 'client-id', 'clientId', 'clientid')
|
|
||||||
Tracker.alias(safe_unicode, 'tid', 'trackingId', 'account')
|
|
||||||
Tracker.alias(safe_unicode, 'uid', 'user-id', 'userId', 'userid')
|
|
||||||
Tracker.alias(safe_unicode, 'uip', 'user-ip', 'userIp', 'ipaddr')
|
|
||||||
Tracker.alias(safe_unicode, 'ua', 'userAgent', 'userAgentOverride', 'user-agent')
|
|
||||||
Tracker.alias(safe_unicode, 'dp', 'page', 'path')
|
|
||||||
Tracker.alias(safe_unicode, 'dt', 'title', 'pagetitle', 'pageTitle' 'page-title')
|
|
||||||
Tracker.alias(safe_unicode, 'dl', 'location')
|
|
||||||
Tracker.alias(safe_unicode, 'dh', 'hostname')
|
|
||||||
Tracker.alias(safe_unicode, 'sc', 'sessioncontrol', 'session-control', 'sessionControl')
|
|
||||||
Tracker.alias(safe_unicode, 'dr', 'referrer', 'referer')
|
|
||||||
Tracker.alias(int, 'qt', 'queueTime', 'queue-time')
|
|
||||||
Tracker.alias(safe_unicode, 't', 'hitType', 'hittype')
|
|
||||||
Tracker.alias(int, 'aip', 'anonymizeIp', 'anonIp', 'anonymize-ip')
|
|
||||||
Tracker.alias(safe_unicode, 'ds', 'dataSource', 'data-source')
|
|
||||||
|
|
||||||
# Campaign attribution
|
|
||||||
Tracker.alias(safe_unicode, 'cn', 'campaign', 'campaignName', 'campaign-name')
|
|
||||||
Tracker.alias(safe_unicode, 'cs', 'source', 'campaignSource', 'campaign-source')
|
|
||||||
Tracker.alias(safe_unicode, 'cm', 'medium', 'campaignMedium', 'campaign-medium')
|
|
||||||
Tracker.alias(safe_unicode, 'ck', 'keyword', 'campaignKeyword', 'campaign-keyword')
|
|
||||||
Tracker.alias(safe_unicode, 'cc', 'content', 'campaignContent', 'campaign-content')
|
|
||||||
Tracker.alias(safe_unicode, 'ci', 'campaignId', 'campaignID', 'campaign-id')
|
|
||||||
|
|
||||||
# Technical specs
|
|
||||||
Tracker.alias(safe_unicode, 'sr', 'screenResolution', 'screen-resolution', 'resolution')
|
|
||||||
Tracker.alias(safe_unicode, 'vp', 'viewport', 'viewportSize', 'viewport-size')
|
|
||||||
Tracker.alias(safe_unicode, 'de', 'encoding', 'documentEncoding', 'document-encoding')
|
|
||||||
Tracker.alias(int, 'sd', 'colors', 'screenColors', 'screen-colors')
|
|
||||||
Tracker.alias(safe_unicode, 'ul', 'language', 'user-language', 'userLanguage')
|
|
||||||
|
|
||||||
# Mobile app
|
|
||||||
Tracker.alias(safe_unicode, 'an', 'appName', 'app-name', 'app')
|
|
||||||
Tracker.alias(safe_unicode, 'cd', 'contentDescription', 'screenName', 'screen-name', 'content-description')
|
|
||||||
Tracker.alias(safe_unicode, 'av', 'appVersion', 'app-version', 'version')
|
|
||||||
Tracker.alias(safe_unicode, 'aid', 'appID', 'appId', 'application-id', 'app-id', 'applicationId')
|
|
||||||
Tracker.alias(safe_unicode, 'aiid', 'appInstallerId', 'app-installer-id')
|
|
||||||
|
|
||||||
# Ecommerce
|
|
||||||
Tracker.alias(safe_unicode, 'ta', 'affiliation', 'transactionAffiliation', 'transaction-affiliation')
|
|
||||||
Tracker.alias(safe_unicode, 'ti', 'transaction', 'transactionId', 'transaction-id')
|
|
||||||
Tracker.alias(float, 'tr', 'revenue', 'transactionRevenue', 'transaction-revenue')
|
|
||||||
Tracker.alias(float, 'ts', 'shipping', 'transactionShipping', 'transaction-shipping')
|
|
||||||
Tracker.alias(float, 'tt', 'tax', 'transactionTax', 'transaction-tax')
|
|
||||||
Tracker.alias(safe_unicode, 'cu', 'currency', 'transactionCurrency',
|
|
||||||
'transaction-currency') # Currency code, e.g. USD, EUR
|
|
||||||
Tracker.alias(safe_unicode, 'in', 'item-name', 'itemName')
|
|
||||||
Tracker.alias(float, 'ip', 'item-price', 'itemPrice')
|
|
||||||
Tracker.alias(float, 'iq', 'item-quantity', 'itemQuantity')
|
|
||||||
Tracker.alias(safe_unicode, 'ic', 'item-code', 'sku', 'itemCode')
|
|
||||||
Tracker.alias(safe_unicode, 'iv', 'item-variation', 'item-category', 'itemCategory', 'itemVariation')
|
|
||||||
|
|
||||||
# Events
|
|
||||||
Tracker.alias(safe_unicode, 'ec', 'event-category', 'eventCategory', 'category')
|
|
||||||
Tracker.alias(safe_unicode, 'ea', 'event-action', 'eventAction', 'action')
|
|
||||||
Tracker.alias(safe_unicode, 'el', 'event-label', 'eventLabel', 'label')
|
|
||||||
Tracker.alias(int, 'ev', 'event-value', 'eventValue', 'value')
|
|
||||||
Tracker.alias(int, 'ni', 'noninteractive', 'nonInteractive', 'noninteraction', 'nonInteraction')
|
|
||||||
|
|
||||||
# Social
|
|
||||||
Tracker.alias(safe_unicode, 'sa', 'social-action', 'socialAction')
|
|
||||||
Tracker.alias(safe_unicode, 'sn', 'social-network', 'socialNetwork')
|
|
||||||
Tracker.alias(safe_unicode, 'st', 'social-target', 'socialTarget')
|
|
||||||
|
|
||||||
# Exceptions
|
|
||||||
Tracker.alias(safe_unicode, 'exd', 'exception-description', 'exceptionDescription', 'exDescription')
|
|
||||||
Tracker.alias(int, 'exf', 'exception-fatal', 'exceptionFatal', 'exFatal')
|
|
||||||
|
|
||||||
# User Timing
|
|
||||||
Tracker.alias(safe_unicode, 'utc', 'timingCategory', 'timing-category')
|
|
||||||
Tracker.alias(safe_unicode, 'utv', 'timingVariable', 'timing-variable')
|
|
||||||
Tracker.alias(float, 'utt', 'time', 'timingTime', 'timing-time')
|
|
||||||
Tracker.alias(safe_unicode, 'utl', 'timingLabel', 'timing-label')
|
|
||||||
Tracker.alias(float, 'dns', 'timingDNS', 'timing-dns')
|
|
||||||
Tracker.alias(float, 'pdt', 'timingPageLoad', 'timing-page-load')
|
|
||||||
Tracker.alias(float, 'rrt', 'timingRedirect', 'timing-redirect')
|
|
||||||
Tracker.alias(safe_unicode, 'tcp', 'timingTCPConnect', 'timing-tcp-connect')
|
|
||||||
Tracker.alias(safe_unicode, 'srt', 'timingServerResponse', 'timing-server-response')
|
|
||||||
|
|
||||||
# Custom dimensions and metrics
|
|
||||||
for i in range(0, 200):
|
|
||||||
Tracker.alias(safe_unicode, 'cd{0}'.format(i), 'dimension{0}'.format(i))
|
|
||||||
Tracker.alias(int, 'cm{0}'.format(i), 'metric{0}'.format(i))
|
|
||||||
|
|
||||||
# Content groups
|
|
||||||
for i in range(0, 5):
|
|
||||||
Tracker.alias(safe_unicode, 'cg{0}'.format(i), 'contentGroup{0}'.format(i))
|
|
||||||
|
|
||||||
# Enhanced Ecommerce
|
|
||||||
Tracker.alias(str, 'pa') # Product action
|
|
||||||
Tracker.alias(str, 'tcc') # Coupon code
|
|
||||||
Tracker.alias(str, 'pal') # Product action list
|
|
||||||
Tracker.alias(int, 'cos') # Checkout step
|
|
||||||
Tracker.alias(str, 'col') # Checkout step option
|
|
||||||
|
|
||||||
Tracker.alias(str, 'promoa') # Promotion action
|
|
||||||
|
|
||||||
for product_index in range(1, MAX_EC_PRODUCTS):
|
|
||||||
Tracker.alias(str, 'pr{0}id'.format(product_index)) # Product SKU
|
|
||||||
Tracker.alias(str, 'pr{0}nm'.format(product_index)) # Product name
|
|
||||||
Tracker.alias(str, 'pr{0}br'.format(product_index)) # Product brand
|
|
||||||
Tracker.alias(str, 'pr{0}ca'.format(product_index)) # Product category
|
|
||||||
Tracker.alias(str, 'pr{0}va'.format(product_index)) # Product variant
|
|
||||||
Tracker.alias(str, 'pr{0}pr'.format(product_index)) # Product price
|
|
||||||
Tracker.alias(int, 'pr{0}qt'.format(product_index)) # Product quantity
|
|
||||||
Tracker.alias(str, 'pr{0}cc'.format(product_index)) # Product coupon code
|
|
||||||
Tracker.alias(int, 'pr{0}ps'.format(product_index)) # Product position
|
|
||||||
|
|
||||||
for custom_index in range(MAX_CUSTOM_DEFINITIONS):
|
|
||||||
Tracker.alias(str, 'pr{0}cd{1}'.format(product_index, custom_index)) # Product custom dimension
|
|
||||||
Tracker.alias(int, 'pr{0}cm{1}'.format(product_index, custom_index)) # Product custom metric
|
|
||||||
|
|
||||||
for list_index in range(1, MAX_EC_LISTS):
|
|
||||||
Tracker.alias(str, 'il{0}pi{1}id'.format(list_index, product_index)) # Product impression SKU
|
|
||||||
Tracker.alias(str, 'il{0}pi{1}nm'.format(list_index, product_index)) # Product impression name
|
|
||||||
Tracker.alias(str, 'il{0}pi{1}br'.format(list_index, product_index)) # Product impression brand
|
|
||||||
Tracker.alias(str, 'il{0}pi{1}ca'.format(list_index, product_index)) # Product impression category
|
|
||||||
Tracker.alias(str, 'il{0}pi{1}va'.format(list_index, product_index)) # Product impression variant
|
|
||||||
Tracker.alias(int, 'il{0}pi{1}ps'.format(list_index, product_index)) # Product impression position
|
|
||||||
Tracker.alias(int, 'il{0}pi{1}pr'.format(list_index, product_index)) # Product impression price
|
|
||||||
|
|
||||||
for custom_index in range(MAX_CUSTOM_DEFINITIONS):
|
|
||||||
Tracker.alias(str, 'il{0}pi{1}cd{2}'.format(list_index, product_index,
|
|
||||||
custom_index)) # Product impression custom dimension
|
|
||||||
Tracker.alias(int, 'il{0}pi{1}cm{2}'.format(list_index, product_index,
|
|
||||||
custom_index)) # Product impression custom metric
|
|
||||||
|
|
||||||
for list_index in range(1, MAX_EC_LISTS):
|
|
||||||
Tracker.alias(str, 'il{0}nm'.format(list_index)) # Product impression list name
|
|
||||||
|
|
||||||
for promotion_index in range(1, MAX_EC_PROMOTIONS):
|
|
||||||
Tracker.alias(str, 'promo{0}id'.format(promotion_index)) # Promotion ID
|
|
||||||
Tracker.alias(str, 'promo{0}nm'.format(promotion_index)) # Promotion name
|
|
||||||
Tracker.alias(str, 'promo{0}cr'.format(promotion_index)) # Promotion creative
|
|
||||||
Tracker.alias(str, 'promo{0}ps'.format(promotion_index)) # Promotion position
|
|
||||||
|
|
||||||
|
|
||||||
# Shortcut for creating trackers
|
|
||||||
def create(account, *args, **kwargs):
|
|
||||||
return Tracker(account, *args, **kwargs)
|
|
||||||
|
|
||||||
# vim: set nowrap tabstop=4 shiftwidth=4 softtabstop=0 expandtab textwidth=0 filetype=python foldmethod=indent foldcolumn=4
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from . import Tracker
|
|
||||||
608
lib/appdirs.py
608
lib/appdirs.py
@@ -1,608 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright (c) 2005-2010 ActiveState Software Inc.
|
|
||||||
# Copyright (c) 2013 Eddy Petrișor
|
|
||||||
|
|
||||||
"""Utilities for determining application-specific dirs.
|
|
||||||
|
|
||||||
See <http://github.com/ActiveState/appdirs> for details and usage.
|
|
||||||
"""
|
|
||||||
# Dev Notes:
|
|
||||||
# - MSDN on where to store app data files:
|
|
||||||
# http://support.microsoft.com/default.aspx?scid=kb;en-us;310294#XSLTH3194121123120121120120
|
|
||||||
# - Mac OS X: http://developer.apple.com/documentation/MacOSX/Conceptual/BPFileSystem/index.html
|
|
||||||
# - XDG spec for Un*x: http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
|
|
||||||
|
|
||||||
__version_info__ = (1, 4, 3)
|
|
||||||
__version__ = '.'.join(map(str, __version_info__))
|
|
||||||
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
PY3 = sys.version_info[0] == 3
|
|
||||||
|
|
||||||
if PY3:
|
|
||||||
unicode = str
|
|
||||||
|
|
||||||
if sys.platform.startswith('java'):
|
|
||||||
import platform
|
|
||||||
os_name = platform.java_ver()[3][0]
|
|
||||||
if os_name.startswith('Windows'): # "Windows XP", "Windows 7", etc.
|
|
||||||
system = 'win32'
|
|
||||||
elif os_name.startswith('Mac'): # "Mac OS X", etc.
|
|
||||||
system = 'darwin'
|
|
||||||
else: # "Linux", "SunOS", "FreeBSD", etc.
|
|
||||||
# Setting this to "linux2" is not ideal, but only Windows or Mac
|
|
||||||
# are actually checked for and the rest of the module expects
|
|
||||||
# *sys.platform* style strings.
|
|
||||||
system = 'linux2'
|
|
||||||
else:
|
|
||||||
system = sys.platform
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def user_data_dir(appname=None, appauthor=None, version=None, roaming=False):
|
|
||||||
r"""Return full path to the user-specific data dir for this application.
|
|
||||||
|
|
||||||
"appname" is the name of application.
|
|
||||||
If None, just the system directory is returned.
|
|
||||||
"appauthor" (only used on Windows) is the name of the
|
|
||||||
appauthor or distributing body for this application. Typically
|
|
||||||
it is the owning company name. This falls back to appname. You may
|
|
||||||
pass False to disable it.
|
|
||||||
"version" is an optional version path element to append to the
|
|
||||||
path. You might want to use this if you want multiple versions
|
|
||||||
of your app to be able to run independently. If used, this
|
|
||||||
would typically be "<major>.<minor>".
|
|
||||||
Only applied when appname is present.
|
|
||||||
"roaming" (boolean, default False) can be set True to use the Windows
|
|
||||||
roaming appdata directory. That means that for users on a Windows
|
|
||||||
network setup for roaming profiles, this user data will be
|
|
||||||
sync'd on login. See
|
|
||||||
<http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>
|
|
||||||
for a discussion of issues.
|
|
||||||
|
|
||||||
Typical user data directories are:
|
|
||||||
Mac OS X: ~/Library/Application Support/<AppName>
|
|
||||||
Unix: ~/.local/share/<AppName> # or in $XDG_DATA_HOME, if defined
|
|
||||||
Win XP (not roaming): C:\Documents and Settings\<username>\Application Data\<AppAuthor>\<AppName>
|
|
||||||
Win XP (roaming): C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName>
|
|
||||||
Win 7 (not roaming): C:\Users\<username>\AppData\Local\<AppAuthor>\<AppName>
|
|
||||||
Win 7 (roaming): C:\Users\<username>\AppData\Roaming\<AppAuthor>\<AppName>
|
|
||||||
|
|
||||||
For Unix, we follow the XDG spec and support $XDG_DATA_HOME.
|
|
||||||
That means, by default "~/.local/share/<AppName>".
|
|
||||||
"""
|
|
||||||
if system == "win32":
|
|
||||||
if appauthor is None:
|
|
||||||
appauthor = appname
|
|
||||||
const = roaming and "CSIDL_APPDATA" or "CSIDL_LOCAL_APPDATA"
|
|
||||||
path = os.path.normpath(_get_win_folder(const))
|
|
||||||
if appname:
|
|
||||||
if appauthor is not False:
|
|
||||||
path = os.path.join(path, appauthor, appname)
|
|
||||||
else:
|
|
||||||
path = os.path.join(path, appname)
|
|
||||||
elif system == 'darwin':
|
|
||||||
path = os.path.expanduser('~/Library/Application Support/')
|
|
||||||
if appname:
|
|
||||||
path = os.path.join(path, appname)
|
|
||||||
else:
|
|
||||||
path = os.getenv('XDG_DATA_HOME', os.path.expanduser("~/.local/share"))
|
|
||||||
if appname:
|
|
||||||
path = os.path.join(path, appname)
|
|
||||||
if appname and version:
|
|
||||||
path = os.path.join(path, version)
|
|
||||||
return path
|
|
||||||
|
|
||||||
|
|
||||||
def site_data_dir(appname=None, appauthor=None, version=None, multipath=False):
|
|
||||||
r"""Return full path to the user-shared data dir for this application.
|
|
||||||
|
|
||||||
"appname" is the name of application.
|
|
||||||
If None, just the system directory is returned.
|
|
||||||
"appauthor" (only used on Windows) is the name of the
|
|
||||||
appauthor or distributing body for this application. Typically
|
|
||||||
it is the owning company name. This falls back to appname. You may
|
|
||||||
pass False to disable it.
|
|
||||||
"version" is an optional version path element to append to the
|
|
||||||
path. You might want to use this if you want multiple versions
|
|
||||||
of your app to be able to run independently. If used, this
|
|
||||||
would typically be "<major>.<minor>".
|
|
||||||
Only applied when appname is present.
|
|
||||||
"multipath" is an optional parameter only applicable to *nix
|
|
||||||
which indicates that the entire list of data dirs should be
|
|
||||||
returned. By default, the first item from XDG_DATA_DIRS is
|
|
||||||
returned, or '/usr/local/share/<AppName>',
|
|
||||||
if XDG_DATA_DIRS is not set
|
|
||||||
|
|
||||||
Typical site data directories are:
|
|
||||||
Mac OS X: /Library/Application Support/<AppName>
|
|
||||||
Unix: /usr/local/share/<AppName> or /usr/share/<AppName>
|
|
||||||
Win XP: C:\Documents and Settings\All Users\Application Data\<AppAuthor>\<AppName>
|
|
||||||
Vista: (Fail! "C:\ProgramData" is a hidden *system* directory on Vista.)
|
|
||||||
Win 7: C:\ProgramData\<AppAuthor>\<AppName> # Hidden, but writeable on Win 7.
|
|
||||||
|
|
||||||
For Unix, this is using the $XDG_DATA_DIRS[0] default.
|
|
||||||
|
|
||||||
WARNING: Do not use this on Windows. See the Vista-Fail note above for why.
|
|
||||||
"""
|
|
||||||
if system == "win32":
|
|
||||||
if appauthor is None:
|
|
||||||
appauthor = appname
|
|
||||||
path = os.path.normpath(_get_win_folder("CSIDL_COMMON_APPDATA"))
|
|
||||||
if appname:
|
|
||||||
if appauthor is not False:
|
|
||||||
path = os.path.join(path, appauthor, appname)
|
|
||||||
else:
|
|
||||||
path = os.path.join(path, appname)
|
|
||||||
elif system == 'darwin':
|
|
||||||
path = os.path.expanduser('/Library/Application Support')
|
|
||||||
if appname:
|
|
||||||
path = os.path.join(path, appname)
|
|
||||||
else:
|
|
||||||
# XDG default for $XDG_DATA_DIRS
|
|
||||||
# only first, if multipath is False
|
|
||||||
path = os.getenv('XDG_DATA_DIRS',
|
|
||||||
os.pathsep.join(['/usr/local/share', '/usr/share']))
|
|
||||||
pathlist = [os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep)]
|
|
||||||
if appname:
|
|
||||||
if version:
|
|
||||||
appname = os.path.join(appname, version)
|
|
||||||
pathlist = [os.sep.join([x, appname]) for x in pathlist]
|
|
||||||
|
|
||||||
if multipath:
|
|
||||||
path = os.pathsep.join(pathlist)
|
|
||||||
else:
|
|
||||||
path = pathlist[0]
|
|
||||||
return path
|
|
||||||
|
|
||||||
if appname and version:
|
|
||||||
path = os.path.join(path, version)
|
|
||||||
return path
|
|
||||||
|
|
||||||
|
|
||||||
def user_config_dir(appname=None, appauthor=None, version=None, roaming=False):
|
|
||||||
r"""Return full path to the user-specific config dir for this application.
|
|
||||||
|
|
||||||
"appname" is the name of application.
|
|
||||||
If None, just the system directory is returned.
|
|
||||||
"appauthor" (only used on Windows) is the name of the
|
|
||||||
appauthor or distributing body for this application. Typically
|
|
||||||
it is the owning company name. This falls back to appname. You may
|
|
||||||
pass False to disable it.
|
|
||||||
"version" is an optional version path element to append to the
|
|
||||||
path. You might want to use this if you want multiple versions
|
|
||||||
of your app to be able to run independently. If used, this
|
|
||||||
would typically be "<major>.<minor>".
|
|
||||||
Only applied when appname is present.
|
|
||||||
"roaming" (boolean, default False) can be set True to use the Windows
|
|
||||||
roaming appdata directory. That means that for users on a Windows
|
|
||||||
network setup for roaming profiles, this user data will be
|
|
||||||
sync'd on login. See
|
|
||||||
<http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>
|
|
||||||
for a discussion of issues.
|
|
||||||
|
|
||||||
Typical user config directories are:
|
|
||||||
Mac OS X: same as user_data_dir
|
|
||||||
Unix: ~/.config/<AppName> # or in $XDG_CONFIG_HOME, if defined
|
|
||||||
Win *: same as user_data_dir
|
|
||||||
|
|
||||||
For Unix, we follow the XDG spec and support $XDG_CONFIG_HOME.
|
|
||||||
That means, by default "~/.config/<AppName>".
|
|
||||||
"""
|
|
||||||
if system in ["win32", "darwin"]:
|
|
||||||
path = user_data_dir(appname, appauthor, None, roaming)
|
|
||||||
else:
|
|
||||||
path = os.getenv('XDG_CONFIG_HOME', os.path.expanduser("~/.config"))
|
|
||||||
if appname:
|
|
||||||
path = os.path.join(path, appname)
|
|
||||||
if appname and version:
|
|
||||||
path = os.path.join(path, version)
|
|
||||||
return path
|
|
||||||
|
|
||||||
|
|
||||||
def site_config_dir(appname=None, appauthor=None, version=None, multipath=False):
|
|
||||||
r"""Return full path to the user-shared data dir for this application.
|
|
||||||
|
|
||||||
"appname" is the name of application.
|
|
||||||
If None, just the system directory is returned.
|
|
||||||
"appauthor" (only used on Windows) is the name of the
|
|
||||||
appauthor or distributing body for this application. Typically
|
|
||||||
it is the owning company name. This falls back to appname. You may
|
|
||||||
pass False to disable it.
|
|
||||||
"version" is an optional version path element to append to the
|
|
||||||
path. You might want to use this if you want multiple versions
|
|
||||||
of your app to be able to run independently. If used, this
|
|
||||||
would typically be "<major>.<minor>".
|
|
||||||
Only applied when appname is present.
|
|
||||||
"multipath" is an optional parameter only applicable to *nix
|
|
||||||
which indicates that the entire list of config dirs should be
|
|
||||||
returned. By default, the first item from XDG_CONFIG_DIRS is
|
|
||||||
returned, or '/etc/xdg/<AppName>', if XDG_CONFIG_DIRS is not set
|
|
||||||
|
|
||||||
Typical site config directories are:
|
|
||||||
Mac OS X: same as site_data_dir
|
|
||||||
Unix: /etc/xdg/<AppName> or $XDG_CONFIG_DIRS[i]/<AppName> for each value in
|
|
||||||
$XDG_CONFIG_DIRS
|
|
||||||
Win *: same as site_data_dir
|
|
||||||
Vista: (Fail! "C:\ProgramData" is a hidden *system* directory on Vista.)
|
|
||||||
|
|
||||||
For Unix, this is using the $XDG_CONFIG_DIRS[0] default, if multipath=False
|
|
||||||
|
|
||||||
WARNING: Do not use this on Windows. See the Vista-Fail note above for why.
|
|
||||||
"""
|
|
||||||
if system in ["win32", "darwin"]:
|
|
||||||
path = site_data_dir(appname, appauthor)
|
|
||||||
if appname and version:
|
|
||||||
path = os.path.join(path, version)
|
|
||||||
else:
|
|
||||||
# XDG default for $XDG_CONFIG_DIRS
|
|
||||||
# only first, if multipath is False
|
|
||||||
path = os.getenv('XDG_CONFIG_DIRS', '/etc/xdg')
|
|
||||||
pathlist = [os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep)]
|
|
||||||
if appname:
|
|
||||||
if version:
|
|
||||||
appname = os.path.join(appname, version)
|
|
||||||
pathlist = [os.sep.join([x, appname]) for x in pathlist]
|
|
||||||
|
|
||||||
if multipath:
|
|
||||||
path = os.pathsep.join(pathlist)
|
|
||||||
else:
|
|
||||||
path = pathlist[0]
|
|
||||||
return path
|
|
||||||
|
|
||||||
|
|
||||||
def user_cache_dir(appname=None, appauthor=None, version=None, opinion=True):
|
|
||||||
r"""Return full path to the user-specific cache dir for this application.
|
|
||||||
|
|
||||||
"appname" is the name of application.
|
|
||||||
If None, just the system directory is returned.
|
|
||||||
"appauthor" (only used on Windows) is the name of the
|
|
||||||
appauthor or distributing body for this application. Typically
|
|
||||||
it is the owning company name. This falls back to appname. You may
|
|
||||||
pass False to disable it.
|
|
||||||
"version" is an optional version path element to append to the
|
|
||||||
path. You might want to use this if you want multiple versions
|
|
||||||
of your app to be able to run independently. If used, this
|
|
||||||
would typically be "<major>.<minor>".
|
|
||||||
Only applied when appname is present.
|
|
||||||
"opinion" (boolean) can be False to disable the appending of
|
|
||||||
"Cache" to the base app data dir for Windows. See
|
|
||||||
discussion below.
|
|
||||||
|
|
||||||
Typical user cache directories are:
|
|
||||||
Mac OS X: ~/Library/Caches/<AppName>
|
|
||||||
Unix: ~/.cache/<AppName> (XDG default)
|
|
||||||
Win XP: C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName>\Cache
|
|
||||||
Vista: C:\Users\<username>\AppData\Local\<AppAuthor>\<AppName>\Cache
|
|
||||||
|
|
||||||
On Windows the only suggestion in the MSDN docs is that local settings go in
|
|
||||||
the `CSIDL_LOCAL_APPDATA` directory. This is identical to the non-roaming
|
|
||||||
app data dir (the default returned by `user_data_dir` above). Apps typically
|
|
||||||
put cache data somewhere *under* the given dir here. Some examples:
|
|
||||||
...\Mozilla\Firefox\Profiles\<ProfileName>\Cache
|
|
||||||
...\Acme\SuperApp\Cache\1.0
|
|
||||||
OPINION: This function appends "Cache" to the `CSIDL_LOCAL_APPDATA` value.
|
|
||||||
This can be disabled with the `opinion=False` option.
|
|
||||||
"""
|
|
||||||
if system == "win32":
|
|
||||||
if appauthor is None:
|
|
||||||
appauthor = appname
|
|
||||||
path = os.path.normpath(_get_win_folder("CSIDL_LOCAL_APPDATA"))
|
|
||||||
if appname:
|
|
||||||
if appauthor is not False:
|
|
||||||
path = os.path.join(path, appauthor, appname)
|
|
||||||
else:
|
|
||||||
path = os.path.join(path, appname)
|
|
||||||
if opinion:
|
|
||||||
path = os.path.join(path, "Cache")
|
|
||||||
elif system == 'darwin':
|
|
||||||
path = os.path.expanduser('~/Library/Caches')
|
|
||||||
if appname:
|
|
||||||
path = os.path.join(path, appname)
|
|
||||||
else:
|
|
||||||
path = os.getenv('XDG_CACHE_HOME', os.path.expanduser('~/.cache'))
|
|
||||||
if appname:
|
|
||||||
path = os.path.join(path, appname)
|
|
||||||
if appname and version:
|
|
||||||
path = os.path.join(path, version)
|
|
||||||
return path
|
|
||||||
|
|
||||||
|
|
||||||
def user_state_dir(appname=None, appauthor=None, version=None, roaming=False):
|
|
||||||
r"""Return full path to the user-specific state dir for this application.
|
|
||||||
|
|
||||||
"appname" is the name of application.
|
|
||||||
If None, just the system directory is returned.
|
|
||||||
"appauthor" (only used on Windows) is the name of the
|
|
||||||
appauthor or distributing body for this application. Typically
|
|
||||||
it is the owning company name. This falls back to appname. You may
|
|
||||||
pass False to disable it.
|
|
||||||
"version" is an optional version path element to append to the
|
|
||||||
path. You might want to use this if you want multiple versions
|
|
||||||
of your app to be able to run independently. If used, this
|
|
||||||
would typically be "<major>.<minor>".
|
|
||||||
Only applied when appname is present.
|
|
||||||
"roaming" (boolean, default False) can be set True to use the Windows
|
|
||||||
roaming appdata directory. That means that for users on a Windows
|
|
||||||
network setup for roaming profiles, this user data will be
|
|
||||||
sync'd on login. See
|
|
||||||
<http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>
|
|
||||||
for a discussion of issues.
|
|
||||||
|
|
||||||
Typical user state directories are:
|
|
||||||
Mac OS X: same as user_data_dir
|
|
||||||
Unix: ~/.local/state/<AppName> # or in $XDG_STATE_HOME, if defined
|
|
||||||
Win *: same as user_data_dir
|
|
||||||
|
|
||||||
For Unix, we follow this Debian proposal <https://wiki.debian.org/XDGBaseDirectorySpecification#state>
|
|
||||||
to extend the XDG spec and support $XDG_STATE_HOME.
|
|
||||||
|
|
||||||
That means, by default "~/.local/state/<AppName>".
|
|
||||||
"""
|
|
||||||
if system in ["win32", "darwin"]:
|
|
||||||
path = user_data_dir(appname, appauthor, None, roaming)
|
|
||||||
else:
|
|
||||||
path = os.getenv('XDG_STATE_HOME', os.path.expanduser("~/.local/state"))
|
|
||||||
if appname:
|
|
||||||
path = os.path.join(path, appname)
|
|
||||||
if appname and version:
|
|
||||||
path = os.path.join(path, version)
|
|
||||||
return path
|
|
||||||
|
|
||||||
|
|
||||||
def user_log_dir(appname=None, appauthor=None, version=None, opinion=True):
|
|
||||||
r"""Return full path to the user-specific log dir for this application.
|
|
||||||
|
|
||||||
"appname" is the name of application.
|
|
||||||
If None, just the system directory is returned.
|
|
||||||
"appauthor" (only used on Windows) is the name of the
|
|
||||||
appauthor or distributing body for this application. Typically
|
|
||||||
it is the owning company name. This falls back to appname. You may
|
|
||||||
pass False to disable it.
|
|
||||||
"version" is an optional version path element to append to the
|
|
||||||
path. You might want to use this if you want multiple versions
|
|
||||||
of your app to be able to run independently. If used, this
|
|
||||||
would typically be "<major>.<minor>".
|
|
||||||
Only applied when appname is present.
|
|
||||||
"opinion" (boolean) can be False to disable the appending of
|
|
||||||
"Logs" to the base app data dir for Windows, and "log" to the
|
|
||||||
base cache dir for Unix. See discussion below.
|
|
||||||
|
|
||||||
Typical user log directories are:
|
|
||||||
Mac OS X: ~/Library/Logs/<AppName>
|
|
||||||
Unix: ~/.cache/<AppName>/log # or under $XDG_CACHE_HOME if defined
|
|
||||||
Win XP: C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName>\Logs
|
|
||||||
Vista: C:\Users\<username>\AppData\Local\<AppAuthor>\<AppName>\Logs
|
|
||||||
|
|
||||||
On Windows the only suggestion in the MSDN docs is that local settings
|
|
||||||
go in the `CSIDL_LOCAL_APPDATA` directory. (Note: I'm interested in
|
|
||||||
examples of what some windows apps use for a logs dir.)
|
|
||||||
|
|
||||||
OPINION: This function appends "Logs" to the `CSIDL_LOCAL_APPDATA`
|
|
||||||
value for Windows and appends "log" to the user cache dir for Unix.
|
|
||||||
This can be disabled with the `opinion=False` option.
|
|
||||||
"""
|
|
||||||
if system == "darwin":
|
|
||||||
path = os.path.join(
|
|
||||||
os.path.expanduser('~/Library/Logs'),
|
|
||||||
appname)
|
|
||||||
elif system == "win32":
|
|
||||||
path = user_data_dir(appname, appauthor, version)
|
|
||||||
version = False
|
|
||||||
if opinion:
|
|
||||||
path = os.path.join(path, "Logs")
|
|
||||||
else:
|
|
||||||
path = user_cache_dir(appname, appauthor, version)
|
|
||||||
version = False
|
|
||||||
if opinion:
|
|
||||||
path = os.path.join(path, "log")
|
|
||||||
if appname and version:
|
|
||||||
path = os.path.join(path, version)
|
|
||||||
return path
|
|
||||||
|
|
||||||
|
|
||||||
class AppDirs(object):
|
|
||||||
"""Convenience wrapper for getting application dirs."""
|
|
||||||
def __init__(self, appname=None, appauthor=None, version=None,
|
|
||||||
roaming=False, multipath=False):
|
|
||||||
self.appname = appname
|
|
||||||
self.appauthor = appauthor
|
|
||||||
self.version = version
|
|
||||||
self.roaming = roaming
|
|
||||||
self.multipath = multipath
|
|
||||||
|
|
||||||
@property
|
|
||||||
def user_data_dir(self):
|
|
||||||
return user_data_dir(self.appname, self.appauthor,
|
|
||||||
version=self.version, roaming=self.roaming)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def site_data_dir(self):
|
|
||||||
return site_data_dir(self.appname, self.appauthor,
|
|
||||||
version=self.version, multipath=self.multipath)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def user_config_dir(self):
|
|
||||||
return user_config_dir(self.appname, self.appauthor,
|
|
||||||
version=self.version, roaming=self.roaming)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def site_config_dir(self):
|
|
||||||
return site_config_dir(self.appname, self.appauthor,
|
|
||||||
version=self.version, multipath=self.multipath)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def user_cache_dir(self):
|
|
||||||
return user_cache_dir(self.appname, self.appauthor,
|
|
||||||
version=self.version)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def user_state_dir(self):
|
|
||||||
return user_state_dir(self.appname, self.appauthor,
|
|
||||||
version=self.version)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def user_log_dir(self):
|
|
||||||
return user_log_dir(self.appname, self.appauthor,
|
|
||||||
version=self.version)
|
|
||||||
|
|
||||||
|
|
||||||
#---- internal support stuff
|
|
||||||
|
|
||||||
def _get_win_folder_from_registry(csidl_name):
|
|
||||||
"""This is a fallback technique at best. I'm not sure if using the
|
|
||||||
registry for this guarantees us the correct answer for all CSIDL_*
|
|
||||||
names.
|
|
||||||
"""
|
|
||||||
if PY3:
|
|
||||||
import winreg as _winreg
|
|
||||||
else:
|
|
||||||
import _winreg
|
|
||||||
|
|
||||||
shell_folder_name = {
|
|
||||||
"CSIDL_APPDATA": "AppData",
|
|
||||||
"CSIDL_COMMON_APPDATA": "Common AppData",
|
|
||||||
"CSIDL_LOCAL_APPDATA": "Local AppData",
|
|
||||||
}[csidl_name]
|
|
||||||
|
|
||||||
key = _winreg.OpenKey(
|
|
||||||
_winreg.HKEY_CURRENT_USER,
|
|
||||||
r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
|
|
||||||
)
|
|
||||||
dir, type = _winreg.QueryValueEx(key, shell_folder_name)
|
|
||||||
return dir
|
|
||||||
|
|
||||||
|
|
||||||
def _get_win_folder_with_pywin32(csidl_name):
|
|
||||||
from win32com.shell import shellcon, shell
|
|
||||||
dir = shell.SHGetFolderPath(0, getattr(shellcon, csidl_name), 0, 0)
|
|
||||||
# Try to make this a unicode path because SHGetFolderPath does
|
|
||||||
# not return unicode strings when there is unicode data in the
|
|
||||||
# path.
|
|
||||||
try:
|
|
||||||
dir = unicode(dir)
|
|
||||||
|
|
||||||
# Downgrade to short path name if have highbit chars. See
|
|
||||||
# <http://bugs.activestate.com/show_bug.cgi?id=85099>.
|
|
||||||
has_high_char = False
|
|
||||||
for c in dir:
|
|
||||||
if ord(c) > 255:
|
|
||||||
has_high_char = True
|
|
||||||
break
|
|
||||||
if has_high_char:
|
|
||||||
try:
|
|
||||||
import win32api
|
|
||||||
dir = win32api.GetShortPathName(dir)
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
except UnicodeError:
|
|
||||||
pass
|
|
||||||
return dir
|
|
||||||
|
|
||||||
|
|
||||||
def _get_win_folder_with_ctypes(csidl_name):
|
|
||||||
import ctypes
|
|
||||||
|
|
||||||
csidl_const = {
|
|
||||||
"CSIDL_APPDATA": 26,
|
|
||||||
"CSIDL_COMMON_APPDATA": 35,
|
|
||||||
"CSIDL_LOCAL_APPDATA": 28,
|
|
||||||
}[csidl_name]
|
|
||||||
|
|
||||||
buf = ctypes.create_unicode_buffer(1024)
|
|
||||||
ctypes.windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf)
|
|
||||||
|
|
||||||
# Downgrade to short path name if have highbit chars. See
|
|
||||||
# <http://bugs.activestate.com/show_bug.cgi?id=85099>.
|
|
||||||
has_high_char = False
|
|
||||||
for c in buf:
|
|
||||||
if ord(c) > 255:
|
|
||||||
has_high_char = True
|
|
||||||
break
|
|
||||||
if has_high_char:
|
|
||||||
buf2 = ctypes.create_unicode_buffer(1024)
|
|
||||||
if ctypes.windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024):
|
|
||||||
buf = buf2
|
|
||||||
|
|
||||||
return buf.value
|
|
||||||
|
|
||||||
def _get_win_folder_with_jna(csidl_name):
|
|
||||||
import array
|
|
||||||
from com.sun import jna
|
|
||||||
from com.sun.jna.platform import win32
|
|
||||||
|
|
||||||
buf_size = win32.WinDef.MAX_PATH * 2
|
|
||||||
buf = array.zeros('c', buf_size)
|
|
||||||
shell = win32.Shell32.INSTANCE
|
|
||||||
shell.SHGetFolderPath(None, getattr(win32.ShlObj, csidl_name), None, win32.ShlObj.SHGFP_TYPE_CURRENT, buf)
|
|
||||||
dir = jna.Native.toString(buf.tostring()).rstrip("\0")
|
|
||||||
|
|
||||||
# Downgrade to short path name if have highbit chars. See
|
|
||||||
# <http://bugs.activestate.com/show_bug.cgi?id=85099>.
|
|
||||||
has_high_char = False
|
|
||||||
for c in dir:
|
|
||||||
if ord(c) > 255:
|
|
||||||
has_high_char = True
|
|
||||||
break
|
|
||||||
if has_high_char:
|
|
||||||
buf = array.zeros('c', buf_size)
|
|
||||||
kernel = win32.Kernel32.INSTANCE
|
|
||||||
if kernel.GetShortPathName(dir, buf, buf_size):
|
|
||||||
dir = jna.Native.toString(buf.tostring()).rstrip("\0")
|
|
||||||
|
|
||||||
return dir
|
|
||||||
|
|
||||||
if system == "win32":
|
|
||||||
try:
|
|
||||||
import win32com.shell
|
|
||||||
_get_win_folder = _get_win_folder_with_pywin32
|
|
||||||
except ImportError:
|
|
||||||
try:
|
|
||||||
from ctypes import windll
|
|
||||||
_get_win_folder = _get_win_folder_with_ctypes
|
|
||||||
except ImportError:
|
|
||||||
try:
|
|
||||||
import com.sun.jna
|
|
||||||
_get_win_folder = _get_win_folder_with_jna
|
|
||||||
except ImportError:
|
|
||||||
_get_win_folder = _get_win_folder_from_registry
|
|
||||||
|
|
||||||
|
|
||||||
#---- self test code
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
appname = "MyApp"
|
|
||||||
appauthor = "MyCompany"
|
|
||||||
|
|
||||||
props = ("user_data_dir",
|
|
||||||
"user_config_dir",
|
|
||||||
"user_cache_dir",
|
|
||||||
"user_state_dir",
|
|
||||||
"user_log_dir",
|
|
||||||
"site_data_dir",
|
|
||||||
"site_config_dir")
|
|
||||||
|
|
||||||
print("-- app dirs %s --" % __version__)
|
|
||||||
|
|
||||||
print("-- app dirs (with optional 'version')")
|
|
||||||
dirs = AppDirs(appname, appauthor, version="1.0")
|
|
||||||
for prop in props:
|
|
||||||
print("%s: %s" % (prop, getattr(dirs, prop)))
|
|
||||||
|
|
||||||
print("\n-- app dirs (without optional 'version')")
|
|
||||||
dirs = AppDirs(appname, appauthor)
|
|
||||||
for prop in props:
|
|
||||||
print("%s: %s" % (prop, getattr(dirs, prop)))
|
|
||||||
|
|
||||||
print("\n-- app dirs (without optional 'appauthor')")
|
|
||||||
dirs = AppDirs(appname)
|
|
||||||
for prop in props:
|
|
||||||
print("%s: %s" % (prop, getattr(dirs, prop)))
|
|
||||||
|
|
||||||
print("\n-- app dirs (with disabled 'appauthor')")
|
|
||||||
dirs = AppDirs(appname, appauthor=False)
|
|
||||||
for prop in props:
|
|
||||||
print("%s: %s" % (prop, getattr(dirs, prop)))
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
from pkg_resources import get_distribution, DistributionNotFound
|
|
||||||
|
|
||||||
try:
|
|
||||||
release = get_distribution('APScheduler').version.split('-')[0]
|
|
||||||
except DistributionNotFound:
|
|
||||||
release = '3.5.0'
|
|
||||||
|
|
||||||
version_info = tuple(int(x) if x.isdigit() else x for x in release.split('.'))
|
|
||||||
version = __version__ = '.'.join(str(x) for x in version_info[:3])
|
|
||||||
del get_distribution, DistributionNotFound
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
__all__ = ('EVENT_SCHEDULER_STARTED', 'EVENT_SCHEDULER_SHUTDOWN', 'EVENT_SCHEDULER_PAUSED',
|
|
||||||
'EVENT_SCHEDULER_RESUMED', 'EVENT_EXECUTOR_ADDED', 'EVENT_EXECUTOR_REMOVED',
|
|
||||||
'EVENT_JOBSTORE_ADDED', 'EVENT_JOBSTORE_REMOVED', 'EVENT_ALL_JOBS_REMOVED',
|
|
||||||
'EVENT_JOB_ADDED', 'EVENT_JOB_REMOVED', 'EVENT_JOB_MODIFIED', 'EVENT_JOB_EXECUTED',
|
|
||||||
'EVENT_JOB_ERROR', 'EVENT_JOB_MISSED', 'EVENT_JOB_SUBMITTED', 'EVENT_JOB_MAX_INSTANCES',
|
|
||||||
'SchedulerEvent', 'JobEvent', 'JobExecutionEvent', 'JobSubmissionEvent')
|
|
||||||
|
|
||||||
|
|
||||||
EVENT_SCHEDULER_STARTED = EVENT_SCHEDULER_START = 2 ** 0
|
|
||||||
EVENT_SCHEDULER_SHUTDOWN = 2 ** 1
|
|
||||||
EVENT_SCHEDULER_PAUSED = 2 ** 2
|
|
||||||
EVENT_SCHEDULER_RESUMED = 2 ** 3
|
|
||||||
EVENT_EXECUTOR_ADDED = 2 ** 4
|
|
||||||
EVENT_EXECUTOR_REMOVED = 2 ** 5
|
|
||||||
EVENT_JOBSTORE_ADDED = 2 ** 6
|
|
||||||
EVENT_JOBSTORE_REMOVED = 2 ** 7
|
|
||||||
EVENT_ALL_JOBS_REMOVED = 2 ** 8
|
|
||||||
EVENT_JOB_ADDED = 2 ** 9
|
|
||||||
EVENT_JOB_REMOVED = 2 ** 10
|
|
||||||
EVENT_JOB_MODIFIED = 2 ** 11
|
|
||||||
EVENT_JOB_EXECUTED = 2 ** 12
|
|
||||||
EVENT_JOB_ERROR = 2 ** 13
|
|
||||||
EVENT_JOB_MISSED = 2 ** 14
|
|
||||||
EVENT_JOB_SUBMITTED = 2 ** 15
|
|
||||||
EVENT_JOB_MAX_INSTANCES = 2 ** 16
|
|
||||||
EVENT_ALL = (EVENT_SCHEDULER_STARTED | EVENT_SCHEDULER_SHUTDOWN | EVENT_SCHEDULER_PAUSED |
|
|
||||||
EVENT_SCHEDULER_RESUMED | EVENT_EXECUTOR_ADDED | EVENT_EXECUTOR_REMOVED |
|
|
||||||
EVENT_JOBSTORE_ADDED | EVENT_JOBSTORE_REMOVED | EVENT_ALL_JOBS_REMOVED |
|
|
||||||
EVENT_JOB_ADDED | EVENT_JOB_REMOVED | EVENT_JOB_MODIFIED | EVENT_JOB_EXECUTED |
|
|
||||||
EVENT_JOB_ERROR | EVENT_JOB_MISSED | EVENT_JOB_SUBMITTED | EVENT_JOB_MAX_INSTANCES)
|
|
||||||
|
|
||||||
|
|
||||||
class SchedulerEvent(object):
|
|
||||||
"""
|
|
||||||
An event that concerns the scheduler itself.
|
|
||||||
|
|
||||||
:ivar code: the type code of this event
|
|
||||||
:ivar alias: alias of the job store or executor that was added or removed (if applicable)
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, code, alias=None):
|
|
||||||
super(SchedulerEvent, self).__init__()
|
|
||||||
self.code = code
|
|
||||||
self.alias = alias
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<%s (code=%d)>' % (self.__class__.__name__, self.code)
|
|
||||||
|
|
||||||
|
|
||||||
class JobEvent(SchedulerEvent):
|
|
||||||
"""
|
|
||||||
An event that concerns a job.
|
|
||||||
|
|
||||||
:ivar code: the type code of this event
|
|
||||||
:ivar job_id: identifier of the job in question
|
|
||||||
:ivar jobstore: alias of the job store containing the job in question
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, code, job_id, jobstore):
|
|
||||||
super(JobEvent, self).__init__(code)
|
|
||||||
self.code = code
|
|
||||||
self.job_id = job_id
|
|
||||||
self.jobstore = jobstore
|
|
||||||
|
|
||||||
|
|
||||||
class JobSubmissionEvent(JobEvent):
|
|
||||||
"""
|
|
||||||
An event that concerns the submission of a job to its executor.
|
|
||||||
|
|
||||||
:ivar scheduled_run_times: a list of datetimes when the job was intended to run
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, code, job_id, jobstore, scheduled_run_times):
|
|
||||||
super(JobSubmissionEvent, self).__init__(code, job_id, jobstore)
|
|
||||||
self.scheduled_run_times = scheduled_run_times
|
|
||||||
|
|
||||||
|
|
||||||
class JobExecutionEvent(JobEvent):
|
|
||||||
"""
|
|
||||||
An event that concerns the running of a job within its executor.
|
|
||||||
|
|
||||||
:ivar scheduled_run_time: the time when the job was scheduled to be run
|
|
||||||
:ivar retval: the return value of the successfully executed job
|
|
||||||
:ivar exception: the exception raised by the job
|
|
||||||
:ivar traceback: a formatted traceback for the exception
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, code, job_id, jobstore, scheduled_run_time, retval=None, exception=None,
|
|
||||||
traceback=None):
|
|
||||||
super(JobExecutionEvent, self).__init__(code, job_id, jobstore)
|
|
||||||
self.scheduled_run_time = scheduled_run_time
|
|
||||||
self.retval = retval
|
|
||||||
self.exception = exception
|
|
||||||
self.traceback = traceback
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from apscheduler.executors.base import BaseExecutor, run_job
|
|
||||||
from apscheduler.util import iscoroutinefunction_partial
|
|
||||||
|
|
||||||
try:
|
|
||||||
from apscheduler.executors.base_py3 import run_coroutine_job
|
|
||||||
except ImportError:
|
|
||||||
run_coroutine_job = None
|
|
||||||
|
|
||||||
|
|
||||||
class AsyncIOExecutor(BaseExecutor):
|
|
||||||
"""
|
|
||||||
Runs jobs in the default executor of the event loop.
|
|
||||||
|
|
||||||
If the job function is a native coroutine function, it is scheduled to be run directly in the
|
|
||||||
event loop as soon as possible. All other functions are run in the event loop's default
|
|
||||||
executor which is usually a thread pool.
|
|
||||||
|
|
||||||
Plugin alias: ``asyncio``
|
|
||||||
"""
|
|
||||||
|
|
||||||
def start(self, scheduler, alias):
|
|
||||||
super(AsyncIOExecutor, self).start(scheduler, alias)
|
|
||||||
self._eventloop = scheduler._eventloop
|
|
||||||
self._pending_futures = set()
|
|
||||||
|
|
||||||
def shutdown(self, wait=True):
|
|
||||||
# There is no way to honor wait=True without converting this method into a coroutine method
|
|
||||||
for f in self._pending_futures:
|
|
||||||
if not f.done():
|
|
||||||
f.cancel()
|
|
||||||
|
|
||||||
self._pending_futures.clear()
|
|
||||||
|
|
||||||
def _do_submit_job(self, job, run_times):
|
|
||||||
def callback(f):
|
|
||||||
self._pending_futures.discard(f)
|
|
||||||
try:
|
|
||||||
events = f.result()
|
|
||||||
except BaseException:
|
|
||||||
self._run_job_error(job.id, *sys.exc_info()[1:])
|
|
||||||
else:
|
|
||||||
self._run_job_success(job.id, events)
|
|
||||||
|
|
||||||
if iscoroutinefunction_partial(job.func):
|
|
||||||
if run_coroutine_job is not None:
|
|
||||||
coro = run_coroutine_job(job, job._jobstore_alias, run_times, self._logger.name)
|
|
||||||
f = self._eventloop.create_task(coro)
|
|
||||||
else:
|
|
||||||
raise Exception('Executing coroutine based jobs is not supported with Trollius')
|
|
||||||
else:
|
|
||||||
f = self._eventloop.run_in_executor(None, run_job, job, job._jobstore_alias, run_times,
|
|
||||||
self._logger.name)
|
|
||||||
|
|
||||||
f.add_done_callback(callback)
|
|
||||||
self._pending_futures.add(f)
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
from abc import ABCMeta, abstractmethod
|
|
||||||
from collections import defaultdict
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from traceback import format_tb
|
|
||||||
import logging
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from pytz import utc
|
|
||||||
import six
|
|
||||||
|
|
||||||
from apscheduler.events import (
|
|
||||||
JobExecutionEvent, EVENT_JOB_MISSED, EVENT_JOB_ERROR, EVENT_JOB_EXECUTED)
|
|
||||||
|
|
||||||
|
|
||||||
class MaxInstancesReachedError(Exception):
|
|
||||||
def __init__(self, job):
|
|
||||||
super(MaxInstancesReachedError, self).__init__(
|
|
||||||
'Job "%s" has already reached its maximum number of instances (%d)' %
|
|
||||||
(job.id, job.max_instances))
|
|
||||||
|
|
||||||
|
|
||||||
class BaseExecutor(six.with_metaclass(ABCMeta, object)):
|
|
||||||
"""Abstract base class that defines the interface that every executor must implement."""
|
|
||||||
|
|
||||||
_scheduler = None
|
|
||||||
_lock = None
|
|
||||||
_logger = logging.getLogger('apscheduler.executors')
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super(BaseExecutor, self).__init__()
|
|
||||||
self._instances = defaultdict(lambda: 0)
|
|
||||||
|
|
||||||
def start(self, scheduler, alias):
|
|
||||||
"""
|
|
||||||
Called by the scheduler when the scheduler is being started or when the executor is being
|
|
||||||
added to an already running scheduler.
|
|
||||||
|
|
||||||
:param apscheduler.schedulers.base.BaseScheduler scheduler: the scheduler that is starting
|
|
||||||
this executor
|
|
||||||
:param str|unicode alias: alias of this executor as it was assigned to the scheduler
|
|
||||||
|
|
||||||
"""
|
|
||||||
self._scheduler = scheduler
|
|
||||||
self._lock = scheduler._create_lock()
|
|
||||||
self._logger = logging.getLogger('apscheduler.executors.%s' % alias)
|
|
||||||
|
|
||||||
def shutdown(self, wait=True):
|
|
||||||
"""
|
|
||||||
Shuts down this executor.
|
|
||||||
|
|
||||||
:param bool wait: ``True`` to wait until all submitted jobs
|
|
||||||
have been executed
|
|
||||||
"""
|
|
||||||
|
|
||||||
def submit_job(self, job, run_times):
|
|
||||||
"""
|
|
||||||
Submits job for execution.
|
|
||||||
|
|
||||||
:param Job job: job to execute
|
|
||||||
:param list[datetime] run_times: list of datetimes specifying
|
|
||||||
when the job should have been run
|
|
||||||
:raises MaxInstancesReachedError: if the maximum number of
|
|
||||||
allowed instances for this job has been reached
|
|
||||||
|
|
||||||
"""
|
|
||||||
assert self._lock is not None, 'This executor has not been started yet'
|
|
||||||
with self._lock:
|
|
||||||
if self._instances[job.id] >= job.max_instances:
|
|
||||||
raise MaxInstancesReachedError(job)
|
|
||||||
|
|
||||||
self._do_submit_job(job, run_times)
|
|
||||||
self._instances[job.id] += 1
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def _do_submit_job(self, job, run_times):
|
|
||||||
"""Performs the actual task of scheduling `run_job` to be called."""
|
|
||||||
|
|
||||||
def _run_job_success(self, job_id, events):
|
|
||||||
"""
|
|
||||||
Called by the executor with the list of generated events when :func:`run_job` has been
|
|
||||||
successfully called.
|
|
||||||
|
|
||||||
"""
|
|
||||||
with self._lock:
|
|
||||||
self._instances[job_id] -= 1
|
|
||||||
if self._instances[job_id] == 0:
|
|
||||||
del self._instances[job_id]
|
|
||||||
|
|
||||||
for event in events:
|
|
||||||
self._scheduler._dispatch_event(event)
|
|
||||||
|
|
||||||
def _run_job_error(self, job_id, exc, traceback=None):
|
|
||||||
"""Called by the executor with the exception if there is an error calling `run_job`."""
|
|
||||||
with self._lock:
|
|
||||||
self._instances[job_id] -= 1
|
|
||||||
if self._instances[job_id] == 0:
|
|
||||||
del self._instances[job_id]
|
|
||||||
|
|
||||||
exc_info = (exc.__class__, exc, traceback)
|
|
||||||
self._logger.error('Error running job %s', job_id, exc_info=exc_info)
|
|
||||||
|
|
||||||
|
|
||||||
def run_job(job, jobstore_alias, run_times, logger_name):
|
|
||||||
"""
|
|
||||||
Called by executors to run the job. Returns a list of scheduler events to be dispatched by the
|
|
||||||
scheduler.
|
|
||||||
|
|
||||||
"""
|
|
||||||
events = []
|
|
||||||
logger = logging.getLogger(logger_name)
|
|
||||||
for run_time in run_times:
|
|
||||||
# See if the job missed its run time window, and handle
|
|
||||||
# possible misfires accordingly
|
|
||||||
if job.misfire_grace_time is not None:
|
|
||||||
difference = datetime.now(utc) - run_time
|
|
||||||
grace_time = timedelta(seconds=job.misfire_grace_time)
|
|
||||||
if difference > grace_time:
|
|
||||||
events.append(JobExecutionEvent(EVENT_JOB_MISSED, job.id, jobstore_alias,
|
|
||||||
run_time))
|
|
||||||
logger.warning('Run time of job "%s" was missed by %s', job, difference)
|
|
||||||
continue
|
|
||||||
|
|
||||||
logger.info('Running job "%s" (scheduled at %s)', job, run_time)
|
|
||||||
try:
|
|
||||||
retval = job.func(*job.args, **job.kwargs)
|
|
||||||
except BaseException:
|
|
||||||
exc, tb = sys.exc_info()[1:]
|
|
||||||
formatted_tb = ''.join(format_tb(tb))
|
|
||||||
events.append(JobExecutionEvent(EVENT_JOB_ERROR, job.id, jobstore_alias, run_time,
|
|
||||||
exception=exc, traceback=formatted_tb))
|
|
||||||
logger.exception('Job "%s" raised an exception', job)
|
|
||||||
|
|
||||||
# This is to prevent cyclic references that would lead to memory leaks
|
|
||||||
if six.PY2:
|
|
||||||
sys.exc_clear()
|
|
||||||
del tb
|
|
||||||
else:
|
|
||||||
import traceback
|
|
||||||
traceback.clear_frames(tb)
|
|
||||||
del tb
|
|
||||||
else:
|
|
||||||
events.append(JobExecutionEvent(EVENT_JOB_EXECUTED, job.id, jobstore_alias, run_time,
|
|
||||||
retval=retval))
|
|
||||||
logger.info('Job "%s" executed successfully', job)
|
|
||||||
|
|
||||||
return events
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import logging
|
|
||||||
import sys
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from traceback import format_tb
|
|
||||||
|
|
||||||
from pytz import utc
|
|
||||||
|
|
||||||
from apscheduler.events import (
|
|
||||||
JobExecutionEvent, EVENT_JOB_MISSED, EVENT_JOB_ERROR, EVENT_JOB_EXECUTED)
|
|
||||||
|
|
||||||
|
|
||||||
async def run_coroutine_job(job, jobstore_alias, run_times, logger_name):
|
|
||||||
"""Coroutine version of run_job()."""
|
|
||||||
events = []
|
|
||||||
logger = logging.getLogger(logger_name)
|
|
||||||
for run_time in run_times:
|
|
||||||
# See if the job missed its run time window, and handle possible misfires accordingly
|
|
||||||
if job.misfire_grace_time is not None:
|
|
||||||
difference = datetime.now(utc) - run_time
|
|
||||||
grace_time = timedelta(seconds=job.misfire_grace_time)
|
|
||||||
if difference > grace_time:
|
|
||||||
events.append(JobExecutionEvent(EVENT_JOB_MISSED, job.id, jobstore_alias,
|
|
||||||
run_time))
|
|
||||||
logger.warning('Run time of job "%s" was missed by %s', job, difference)
|
|
||||||
continue
|
|
||||||
|
|
||||||
logger.info('Running job "%s" (scheduled at %s)', job, run_time)
|
|
||||||
try:
|
|
||||||
retval = await job.func(*job.args, **job.kwargs)
|
|
||||||
except BaseException:
|
|
||||||
exc, tb = sys.exc_info()[1:]
|
|
||||||
formatted_tb = ''.join(format_tb(tb))
|
|
||||||
events.append(JobExecutionEvent(EVENT_JOB_ERROR, job.id, jobstore_alias, run_time,
|
|
||||||
exception=exc, traceback=formatted_tb))
|
|
||||||
logger.exception('Job "%s" raised an exception', job)
|
|
||||||
else:
|
|
||||||
events.append(JobExecutionEvent(EVENT_JOB_EXECUTED, job.id, jobstore_alias, run_time,
|
|
||||||
retval=retval))
|
|
||||||
logger.info('Job "%s" executed successfully', job)
|
|
||||||
|
|
||||||
return events
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import sys
|
|
||||||
|
|
||||||
from apscheduler.executors.base import BaseExecutor, run_job
|
|
||||||
|
|
||||||
|
|
||||||
class DebugExecutor(BaseExecutor):
|
|
||||||
"""
|
|
||||||
A special executor that executes the target callable directly instead of deferring it to a
|
|
||||||
thread or process.
|
|
||||||
|
|
||||||
Plugin alias: ``debug``
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _do_submit_job(self, job, run_times):
|
|
||||||
try:
|
|
||||||
events = run_job(job, job._jobstore_alias, run_times, self._logger.name)
|
|
||||||
except BaseException:
|
|
||||||
self._run_job_error(job.id, *sys.exc_info()[1:])
|
|
||||||
else:
|
|
||||||
self._run_job_success(job.id, events)
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
from __future__ import absolute_import
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from apscheduler.executors.base import BaseExecutor, run_job
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
import gevent
|
|
||||||
except ImportError: # pragma: nocover
|
|
||||||
raise ImportError('GeventExecutor requires gevent installed')
|
|
||||||
|
|
||||||
|
|
||||||
class GeventExecutor(BaseExecutor):
|
|
||||||
"""
|
|
||||||
Runs jobs as greenlets.
|
|
||||||
|
|
||||||
Plugin alias: ``gevent``
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _do_submit_job(self, job, run_times):
|
|
||||||
def callback(greenlet):
|
|
||||||
try:
|
|
||||||
events = greenlet.get()
|
|
||||||
except BaseException:
|
|
||||||
self._run_job_error(job.id, *sys.exc_info()[1:])
|
|
||||||
else:
|
|
||||||
self._run_job_success(job.id, events)
|
|
||||||
|
|
||||||
gevent.spawn(run_job, job, job._jobstore_alias, run_times, self._logger.name).\
|
|
||||||
link(callback)
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
from abc import abstractmethod
|
|
||||||
import concurrent.futures
|
|
||||||
|
|
||||||
from apscheduler.executors.base import BaseExecutor, run_job
|
|
||||||
|
|
||||||
|
|
||||||
class BasePoolExecutor(BaseExecutor):
|
|
||||||
@abstractmethod
|
|
||||||
def __init__(self, pool):
|
|
||||||
super(BasePoolExecutor, self).__init__()
|
|
||||||
self._pool = pool
|
|
||||||
|
|
||||||
def _do_submit_job(self, job, run_times):
|
|
||||||
def callback(f):
|
|
||||||
exc, tb = (f.exception_info() if hasattr(f, 'exception_info') else
|
|
||||||
(f.exception(), getattr(f.exception(), '__traceback__', None)))
|
|
||||||
if exc:
|
|
||||||
self._run_job_error(job.id, exc, tb)
|
|
||||||
else:
|
|
||||||
self._run_job_success(job.id, f.result())
|
|
||||||
|
|
||||||
f = self._pool.submit(run_job, job, job._jobstore_alias, run_times, self._logger.name)
|
|
||||||
f.add_done_callback(callback)
|
|
||||||
|
|
||||||
def shutdown(self, wait=True):
|
|
||||||
self._pool.shutdown(wait)
|
|
||||||
|
|
||||||
|
|
||||||
class ThreadPoolExecutor(BasePoolExecutor):
|
|
||||||
"""
|
|
||||||
An executor that runs jobs in a concurrent.futures thread pool.
|
|
||||||
|
|
||||||
Plugin alias: ``threadpool``
|
|
||||||
|
|
||||||
:param max_workers: the maximum number of spawned threads.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, max_workers=10):
|
|
||||||
pool = concurrent.futures.ThreadPoolExecutor(int(max_workers))
|
|
||||||
super(ThreadPoolExecutor, self).__init__(pool)
|
|
||||||
|
|
||||||
|
|
||||||
class ProcessPoolExecutor(BasePoolExecutor):
|
|
||||||
"""
|
|
||||||
An executor that runs jobs in a concurrent.futures process pool.
|
|
||||||
|
|
||||||
Plugin alias: ``processpool``
|
|
||||||
|
|
||||||
:param max_workers: the maximum number of spawned processes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, max_workers=10):
|
|
||||||
pool = concurrent.futures.ProcessPoolExecutor(int(max_workers))
|
|
||||||
super(ProcessPoolExecutor, self).__init__(pool)
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
|
||||||
|
|
||||||
from tornado.gen import convert_yielded
|
|
||||||
|
|
||||||
from apscheduler.executors.base import BaseExecutor, run_job
|
|
||||||
|
|
||||||
try:
|
|
||||||
from apscheduler.executors.base_py3 import run_coroutine_job
|
|
||||||
from apscheduler.util import iscoroutinefunction_partial
|
|
||||||
except ImportError:
|
|
||||||
def iscoroutinefunction_partial(func):
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class TornadoExecutor(BaseExecutor):
|
|
||||||
"""
|
|
||||||
Runs jobs either in a thread pool or directly on the I/O loop.
|
|
||||||
|
|
||||||
If the job function is a native coroutine function, it is scheduled to be run directly in the
|
|
||||||
I/O loop as soon as possible. All other functions are run in a thread pool.
|
|
||||||
|
|
||||||
Plugin alias: ``tornado``
|
|
||||||
|
|
||||||
:param int max_workers: maximum number of worker threads in the thread pool
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, max_workers=10):
|
|
||||||
super(TornadoExecutor, self).__init__()
|
|
||||||
self.executor = ThreadPoolExecutor(max_workers)
|
|
||||||
|
|
||||||
def start(self, scheduler, alias):
|
|
||||||
super(TornadoExecutor, self).start(scheduler, alias)
|
|
||||||
self._ioloop = scheduler._ioloop
|
|
||||||
|
|
||||||
def _do_submit_job(self, job, run_times):
|
|
||||||
def callback(f):
|
|
||||||
try:
|
|
||||||
events = f.result()
|
|
||||||
except BaseException:
|
|
||||||
self._run_job_error(job.id, *sys.exc_info()[1:])
|
|
||||||
else:
|
|
||||||
self._run_job_success(job.id, events)
|
|
||||||
|
|
||||||
if iscoroutinefunction_partial(job.func):
|
|
||||||
f = run_coroutine_job(job, job._jobstore_alias, run_times, self._logger.name)
|
|
||||||
else:
|
|
||||||
f = self.executor.submit(run_job, job, job._jobstore_alias, run_times,
|
|
||||||
self._logger.name)
|
|
||||||
|
|
||||||
f = convert_yielded(f)
|
|
||||||
f.add_done_callback(callback)
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
from apscheduler.executors.base import BaseExecutor, run_job
|
|
||||||
|
|
||||||
|
|
||||||
class TwistedExecutor(BaseExecutor):
|
|
||||||
"""
|
|
||||||
Runs jobs in the reactor's thread pool.
|
|
||||||
|
|
||||||
Plugin alias: ``twisted``
|
|
||||||
"""
|
|
||||||
|
|
||||||
def start(self, scheduler, alias):
|
|
||||||
super(TwistedExecutor, self).start(scheduler, alias)
|
|
||||||
self._reactor = scheduler._reactor
|
|
||||||
|
|
||||||
def _do_submit_job(self, job, run_times):
|
|
||||||
def callback(success, result):
|
|
||||||
if success:
|
|
||||||
self._run_job_success(job.id, result)
|
|
||||||
else:
|
|
||||||
self._run_job_error(job.id, result.value, result.tb)
|
|
||||||
|
|
||||||
self._reactor.getThreadPool().callInThreadWithCallback(
|
|
||||||
callback, run_job, job, job._jobstore_alias, run_times, self._logger.name)
|
|
||||||
@@ -1,301 +0,0 @@
|
|||||||
from inspect import ismethod, isclass
|
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from apscheduler.triggers.base import BaseTrigger
|
|
||||||
from apscheduler.util import (
|
|
||||||
ref_to_obj, obj_to_ref, datetime_repr, repr_escape, get_callable_name, check_callable_args,
|
|
||||||
convert_to_datetime)
|
|
||||||
|
|
||||||
try:
|
|
||||||
from collections.abc import Iterable, Mapping
|
|
||||||
except ImportError:
|
|
||||||
from collections import Iterable, Mapping
|
|
||||||
|
|
||||||
|
|
||||||
class Job(object):
|
|
||||||
"""
|
|
||||||
Contains the options given when scheduling callables and its current schedule and other state.
|
|
||||||
This class should never be instantiated by the user.
|
|
||||||
|
|
||||||
:var str id: the unique identifier of this job
|
|
||||||
:var str name: the description of this job
|
|
||||||
:var func: the callable to execute
|
|
||||||
:var tuple|list args: positional arguments to the callable
|
|
||||||
:var dict kwargs: keyword arguments to the callable
|
|
||||||
:var bool coalesce: whether to only run the job once when several run times are due
|
|
||||||
:var trigger: the trigger object that controls the schedule of this job
|
|
||||||
:var str executor: the name of the executor that will run this job
|
|
||||||
:var int misfire_grace_time: the time (in seconds) how much this job's execution is allowed to
|
|
||||||
be late
|
|
||||||
:var int max_instances: the maximum number of concurrently executing instances allowed for this
|
|
||||||
job
|
|
||||||
:var datetime.datetime next_run_time: the next scheduled run time of this job
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
The ``misfire_grace_time`` has some non-obvious effects on job execution. See the
|
|
||||||
:ref:`missed-job-executions` section in the documentation for an in-depth explanation.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ('_scheduler', '_jobstore_alias', 'id', 'trigger', 'executor', 'func', 'func_ref',
|
|
||||||
'args', 'kwargs', 'name', 'misfire_grace_time', 'coalesce', 'max_instances',
|
|
||||||
'next_run_time')
|
|
||||||
|
|
||||||
def __init__(self, scheduler, id=None, **kwargs):
|
|
||||||
super(Job, self).__init__()
|
|
||||||
self._scheduler = scheduler
|
|
||||||
self._jobstore_alias = None
|
|
||||||
self._modify(id=id or uuid4().hex, **kwargs)
|
|
||||||
|
|
||||||
def modify(self, **changes):
|
|
||||||
"""
|
|
||||||
Makes the given changes to this job and saves it in the associated job store.
|
|
||||||
|
|
||||||
Accepted keyword arguments are the same as the variables on this class.
|
|
||||||
|
|
||||||
.. seealso:: :meth:`~apscheduler.schedulers.base.BaseScheduler.modify_job`
|
|
||||||
|
|
||||||
:return Job: this job instance
|
|
||||||
|
|
||||||
"""
|
|
||||||
self._scheduler.modify_job(self.id, self._jobstore_alias, **changes)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def reschedule(self, trigger, **trigger_args):
|
|
||||||
"""
|
|
||||||
Shortcut for switching the trigger on this job.
|
|
||||||
|
|
||||||
.. seealso:: :meth:`~apscheduler.schedulers.base.BaseScheduler.reschedule_job`
|
|
||||||
|
|
||||||
:return Job: this job instance
|
|
||||||
|
|
||||||
"""
|
|
||||||
self._scheduler.reschedule_job(self.id, self._jobstore_alias, trigger, **trigger_args)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def pause(self):
|
|
||||||
"""
|
|
||||||
Temporarily suspend the execution of this job.
|
|
||||||
|
|
||||||
.. seealso:: :meth:`~apscheduler.schedulers.base.BaseScheduler.pause_job`
|
|
||||||
|
|
||||||
:return Job: this job instance
|
|
||||||
|
|
||||||
"""
|
|
||||||
self._scheduler.pause_job(self.id, self._jobstore_alias)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def resume(self):
|
|
||||||
"""
|
|
||||||
Resume the schedule of this job if previously paused.
|
|
||||||
|
|
||||||
.. seealso:: :meth:`~apscheduler.schedulers.base.BaseScheduler.resume_job`
|
|
||||||
|
|
||||||
:return Job: this job instance
|
|
||||||
|
|
||||||
"""
|
|
||||||
self._scheduler.resume_job(self.id, self._jobstore_alias)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def remove(self):
|
|
||||||
"""
|
|
||||||
Unschedules this job and removes it from its associated job store.
|
|
||||||
|
|
||||||
.. seealso:: :meth:`~apscheduler.schedulers.base.BaseScheduler.remove_job`
|
|
||||||
|
|
||||||
"""
|
|
||||||
self._scheduler.remove_job(self.id, self._jobstore_alias)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def pending(self):
|
|
||||||
"""
|
|
||||||
Returns ``True`` if the referenced job is still waiting to be added to its designated job
|
|
||||||
store.
|
|
||||||
|
|
||||||
"""
|
|
||||||
return self._jobstore_alias is None
|
|
||||||
|
|
||||||
#
|
|
||||||
# Private API
|
|
||||||
#
|
|
||||||
|
|
||||||
def _get_run_times(self, now):
|
|
||||||
"""
|
|
||||||
Computes the scheduled run times between ``next_run_time`` and ``now`` (inclusive).
|
|
||||||
|
|
||||||
:type now: datetime.datetime
|
|
||||||
:rtype: list[datetime.datetime]
|
|
||||||
|
|
||||||
"""
|
|
||||||
run_times = []
|
|
||||||
next_run_time = self.next_run_time
|
|
||||||
while next_run_time and next_run_time <= now:
|
|
||||||
run_times.append(next_run_time)
|
|
||||||
next_run_time = self.trigger.get_next_fire_time(next_run_time, now)
|
|
||||||
|
|
||||||
return run_times
|
|
||||||
|
|
||||||
def _modify(self, **changes):
|
|
||||||
"""
|
|
||||||
Validates the changes to the Job and makes the modifications if and only if all of them
|
|
||||||
validate.
|
|
||||||
|
|
||||||
"""
|
|
||||||
approved = {}
|
|
||||||
|
|
||||||
if 'id' in changes:
|
|
||||||
value = changes.pop('id')
|
|
||||||
if not isinstance(value, six.string_types):
|
|
||||||
raise TypeError("id must be a nonempty string")
|
|
||||||
if hasattr(self, 'id'):
|
|
||||||
raise ValueError('The job ID may not be changed')
|
|
||||||
approved['id'] = value
|
|
||||||
|
|
||||||
if 'func' in changes or 'args' in changes or 'kwargs' in changes:
|
|
||||||
func = changes.pop('func') if 'func' in changes else self.func
|
|
||||||
args = changes.pop('args') if 'args' in changes else self.args
|
|
||||||
kwargs = changes.pop('kwargs') if 'kwargs' in changes else self.kwargs
|
|
||||||
|
|
||||||
if isinstance(func, six.string_types):
|
|
||||||
func_ref = func
|
|
||||||
func = ref_to_obj(func)
|
|
||||||
elif callable(func):
|
|
||||||
try:
|
|
||||||
func_ref = obj_to_ref(func)
|
|
||||||
except ValueError:
|
|
||||||
# If this happens, this Job won't be serializable
|
|
||||||
func_ref = None
|
|
||||||
else:
|
|
||||||
raise TypeError('func must be a callable or a textual reference to one')
|
|
||||||
|
|
||||||
if not hasattr(self, 'name') and changes.get('name', None) is None:
|
|
||||||
changes['name'] = get_callable_name(func)
|
|
||||||
|
|
||||||
if isinstance(args, six.string_types) or not isinstance(args, Iterable):
|
|
||||||
raise TypeError('args must be a non-string iterable')
|
|
||||||
if isinstance(kwargs, six.string_types) or not isinstance(kwargs, Mapping):
|
|
||||||
raise TypeError('kwargs must be a dict-like object')
|
|
||||||
|
|
||||||
check_callable_args(func, args, kwargs)
|
|
||||||
|
|
||||||
approved['func'] = func
|
|
||||||
approved['func_ref'] = func_ref
|
|
||||||
approved['args'] = args
|
|
||||||
approved['kwargs'] = kwargs
|
|
||||||
|
|
||||||
if 'name' in changes:
|
|
||||||
value = changes.pop('name')
|
|
||||||
if not value or not isinstance(value, six.string_types):
|
|
||||||
raise TypeError("name must be a nonempty string")
|
|
||||||
approved['name'] = value
|
|
||||||
|
|
||||||
if 'misfire_grace_time' in changes:
|
|
||||||
value = changes.pop('misfire_grace_time')
|
|
||||||
if value is not None and (not isinstance(value, six.integer_types) or value <= 0):
|
|
||||||
raise TypeError('misfire_grace_time must be either None or a positive integer')
|
|
||||||
approved['misfire_grace_time'] = value
|
|
||||||
|
|
||||||
if 'coalesce' in changes:
|
|
||||||
value = bool(changes.pop('coalesce'))
|
|
||||||
approved['coalesce'] = value
|
|
||||||
|
|
||||||
if 'max_instances' in changes:
|
|
||||||
value = changes.pop('max_instances')
|
|
||||||
if not isinstance(value, six.integer_types) or value <= 0:
|
|
||||||
raise TypeError('max_instances must be a positive integer')
|
|
||||||
approved['max_instances'] = value
|
|
||||||
|
|
||||||
if 'trigger' in changes:
|
|
||||||
trigger = changes.pop('trigger')
|
|
||||||
if not isinstance(trigger, BaseTrigger):
|
|
||||||
raise TypeError('Expected a trigger instance, got %s instead' %
|
|
||||||
trigger.__class__.__name__)
|
|
||||||
|
|
||||||
approved['trigger'] = trigger
|
|
||||||
|
|
||||||
if 'executor' in changes:
|
|
||||||
value = changes.pop('executor')
|
|
||||||
if not isinstance(value, six.string_types):
|
|
||||||
raise TypeError('executor must be a string')
|
|
||||||
approved['executor'] = value
|
|
||||||
|
|
||||||
if 'next_run_time' in changes:
|
|
||||||
value = changes.pop('next_run_time')
|
|
||||||
approved['next_run_time'] = convert_to_datetime(value, self._scheduler.timezone,
|
|
||||||
'next_run_time')
|
|
||||||
|
|
||||||
if changes:
|
|
||||||
raise AttributeError('The following are not modifiable attributes of Job: %s' %
|
|
||||||
', '.join(changes))
|
|
||||||
|
|
||||||
for key, value in six.iteritems(approved):
|
|
||||||
setattr(self, key, value)
|
|
||||||
|
|
||||||
def __getstate__(self):
|
|
||||||
# Don't allow this Job to be serialized if the function reference could not be determined
|
|
||||||
if not self.func_ref:
|
|
||||||
raise ValueError(
|
|
||||||
'This Job cannot be serialized since the reference to its callable (%r) could not '
|
|
||||||
'be determined. Consider giving a textual reference (module:function name) '
|
|
||||||
'instead.' % (self.func,))
|
|
||||||
|
|
||||||
# Instance methods cannot survive serialization as-is, so store the "self" argument
|
|
||||||
# explicitly
|
|
||||||
if ismethod(self.func) and not isclass(self.func.__self__):
|
|
||||||
args = (self.func.__self__,) + tuple(self.args)
|
|
||||||
else:
|
|
||||||
args = self.args
|
|
||||||
|
|
||||||
return {
|
|
||||||
'version': 1,
|
|
||||||
'id': self.id,
|
|
||||||
'func': self.func_ref,
|
|
||||||
'trigger': self.trigger,
|
|
||||||
'executor': self.executor,
|
|
||||||
'args': args,
|
|
||||||
'kwargs': self.kwargs,
|
|
||||||
'name': self.name,
|
|
||||||
'misfire_grace_time': self.misfire_grace_time,
|
|
||||||
'coalesce': self.coalesce,
|
|
||||||
'max_instances': self.max_instances,
|
|
||||||
'next_run_time': self.next_run_time
|
|
||||||
}
|
|
||||||
|
|
||||||
def __setstate__(self, state):
|
|
||||||
if state.get('version', 1) > 1:
|
|
||||||
raise ValueError('Job has version %s, but only version 1 can be handled' %
|
|
||||||
state['version'])
|
|
||||||
|
|
||||||
self.id = state['id']
|
|
||||||
self.func_ref = state['func']
|
|
||||||
self.func = ref_to_obj(self.func_ref)
|
|
||||||
self.trigger = state['trigger']
|
|
||||||
self.executor = state['executor']
|
|
||||||
self.args = state['args']
|
|
||||||
self.kwargs = state['kwargs']
|
|
||||||
self.name = state['name']
|
|
||||||
self.misfire_grace_time = state['misfire_grace_time']
|
|
||||||
self.coalesce = state['coalesce']
|
|
||||||
self.max_instances = state['max_instances']
|
|
||||||
self.next_run_time = state['next_run_time']
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
if isinstance(other, Job):
|
|
||||||
return self.id == other.id
|
|
||||||
return NotImplemented
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<Job (id=%s name=%s)>' % (repr_escape(self.id), repr_escape(self.name))
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return repr_escape(self.__unicode__())
|
|
||||||
|
|
||||||
def __unicode__(self):
|
|
||||||
if hasattr(self, 'next_run_time'):
|
|
||||||
status = ('next run at: ' + datetime_repr(self.next_run_time) if
|
|
||||||
self.next_run_time else 'paused')
|
|
||||||
else:
|
|
||||||
status = 'pending'
|
|
||||||
|
|
||||||
return u'%s (trigger: %s, %s)' % (self.name, self.trigger, status)
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
from abc import ABCMeta, abstractmethod
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
|
|
||||||
class JobLookupError(KeyError):
|
|
||||||
"""Raised when the job store cannot find a job for update or removal."""
|
|
||||||
|
|
||||||
def __init__(self, job_id):
|
|
||||||
super(JobLookupError, self).__init__(u'No job by the id of %s was found' % job_id)
|
|
||||||
|
|
||||||
|
|
||||||
class ConflictingIdError(KeyError):
|
|
||||||
"""Raised when the uniqueness of job IDs is being violated."""
|
|
||||||
|
|
||||||
def __init__(self, job_id):
|
|
||||||
super(ConflictingIdError, self).__init__(
|
|
||||||
u'Job identifier (%s) conflicts with an existing job' % job_id)
|
|
||||||
|
|
||||||
|
|
||||||
class TransientJobError(ValueError):
|
|
||||||
"""
|
|
||||||
Raised when an attempt to add transient (with no func_ref) job to a persistent job store is
|
|
||||||
detected.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, job_id):
|
|
||||||
super(TransientJobError, self).__init__(
|
|
||||||
u'Job (%s) cannot be added to this job store because a reference to the callable '
|
|
||||||
u'could not be determined.' % job_id)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseJobStore(six.with_metaclass(ABCMeta)):
|
|
||||||
"""Abstract base class that defines the interface that every job store must implement."""
|
|
||||||
|
|
||||||
_scheduler = None
|
|
||||||
_alias = None
|
|
||||||
_logger = logging.getLogger('apscheduler.jobstores')
|
|
||||||
|
|
||||||
def start(self, scheduler, alias):
|
|
||||||
"""
|
|
||||||
Called by the scheduler when the scheduler is being started or when the job store is being
|
|
||||||
added to an already running scheduler.
|
|
||||||
|
|
||||||
:param apscheduler.schedulers.base.BaseScheduler scheduler: the scheduler that is starting
|
|
||||||
this job store
|
|
||||||
:param str|unicode alias: alias of this job store as it was assigned to the scheduler
|
|
||||||
"""
|
|
||||||
|
|
||||||
self._scheduler = scheduler
|
|
||||||
self._alias = alias
|
|
||||||
self._logger = logging.getLogger('apscheduler.jobstores.%s' % alias)
|
|
||||||
|
|
||||||
def shutdown(self):
|
|
||||||
"""Frees any resources still bound to this job store."""
|
|
||||||
|
|
||||||
def _fix_paused_jobs_sorting(self, jobs):
|
|
||||||
for i, job in enumerate(jobs):
|
|
||||||
if job.next_run_time is not None:
|
|
||||||
if i > 0:
|
|
||||||
paused_jobs = jobs[:i]
|
|
||||||
del jobs[:i]
|
|
||||||
jobs.extend(paused_jobs)
|
|
||||||
break
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def lookup_job(self, job_id):
|
|
||||||
"""
|
|
||||||
Returns a specific job, or ``None`` if it isn't found..
|
|
||||||
|
|
||||||
The job store is responsible for setting the ``scheduler`` and ``jobstore`` attributes of
|
|
||||||
the returned job to point to the scheduler and itself, respectively.
|
|
||||||
|
|
||||||
:param str|unicode job_id: identifier of the job
|
|
||||||
:rtype: Job
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_due_jobs(self, now):
|
|
||||||
"""
|
|
||||||
Returns the list of jobs that have ``next_run_time`` earlier or equal to ``now``.
|
|
||||||
The returned jobs must be sorted by next run time (ascending).
|
|
||||||
|
|
||||||
:param datetime.datetime now: the current (timezone aware) datetime
|
|
||||||
:rtype: list[Job]
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_next_run_time(self):
|
|
||||||
"""
|
|
||||||
Returns the earliest run time of all the jobs stored in this job store, or ``None`` if
|
|
||||||
there are no active jobs.
|
|
||||||
|
|
||||||
:rtype: datetime.datetime
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_all_jobs(self):
|
|
||||||
"""
|
|
||||||
Returns a list of all jobs in this job store.
|
|
||||||
The returned jobs should be sorted by next run time (ascending).
|
|
||||||
Paused jobs (next_run_time == None) should be sorted last.
|
|
||||||
|
|
||||||
The job store is responsible for setting the ``scheduler`` and ``jobstore`` attributes of
|
|
||||||
the returned jobs to point to the scheduler and itself, respectively.
|
|
||||||
|
|
||||||
:rtype: list[Job]
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def add_job(self, job):
|
|
||||||
"""
|
|
||||||
Adds the given job to this store.
|
|
||||||
|
|
||||||
:param Job job: the job to add
|
|
||||||
:raises ConflictingIdError: if there is another job in this store with the same ID
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def update_job(self, job):
|
|
||||||
"""
|
|
||||||
Replaces the job in the store with the given newer version.
|
|
||||||
|
|
||||||
:param Job job: the job to update
|
|
||||||
:raises JobLookupError: if the job does not exist
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def remove_job(self, job_id):
|
|
||||||
"""
|
|
||||||
Removes the given job from this store.
|
|
||||||
|
|
||||||
:param str|unicode job_id: identifier of the job
|
|
||||||
:raises JobLookupError: if the job does not exist
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def remove_all_jobs(self):
|
|
||||||
"""Removes all jobs from this store."""
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<%s>' % self.__class__.__name__
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
from apscheduler.jobstores.base import BaseJobStore, JobLookupError, ConflictingIdError
|
|
||||||
from apscheduler.util import datetime_to_utc_timestamp
|
|
||||||
|
|
||||||
|
|
||||||
class MemoryJobStore(BaseJobStore):
|
|
||||||
"""
|
|
||||||
Stores jobs in an array in RAM. Provides no persistence support.
|
|
||||||
|
|
||||||
Plugin alias: ``memory``
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super(MemoryJobStore, self).__init__()
|
|
||||||
# list of (job, timestamp), sorted by next_run_time and job id (ascending)
|
|
||||||
self._jobs = []
|
|
||||||
self._jobs_index = {} # id -> (job, timestamp) lookup table
|
|
||||||
|
|
||||||
def lookup_job(self, job_id):
|
|
||||||
return self._jobs_index.get(job_id, (None, None))[0]
|
|
||||||
|
|
||||||
def get_due_jobs(self, now):
|
|
||||||
now_timestamp = datetime_to_utc_timestamp(now)
|
|
||||||
pending = []
|
|
||||||
for job, timestamp in self._jobs:
|
|
||||||
if timestamp is None or timestamp > now_timestamp:
|
|
||||||
break
|
|
||||||
pending.append(job)
|
|
||||||
|
|
||||||
return pending
|
|
||||||
|
|
||||||
def get_next_run_time(self):
|
|
||||||
return self._jobs[0][0].next_run_time if self._jobs else None
|
|
||||||
|
|
||||||
def get_all_jobs(self):
|
|
||||||
return [j[0] for j in self._jobs]
|
|
||||||
|
|
||||||
def add_job(self, job):
|
|
||||||
if job.id in self._jobs_index:
|
|
||||||
raise ConflictingIdError(job.id)
|
|
||||||
|
|
||||||
timestamp = datetime_to_utc_timestamp(job.next_run_time)
|
|
||||||
index = self._get_job_index(timestamp, job.id)
|
|
||||||
self._jobs.insert(index, (job, timestamp))
|
|
||||||
self._jobs_index[job.id] = (job, timestamp)
|
|
||||||
|
|
||||||
def update_job(self, job):
|
|
||||||
old_job, old_timestamp = self._jobs_index.get(job.id, (None, None))
|
|
||||||
if old_job is None:
|
|
||||||
raise JobLookupError(job.id)
|
|
||||||
|
|
||||||
# If the next run time has not changed, simply replace the job in its present index.
|
|
||||||
# Otherwise, reinsert the job to the list to preserve the ordering.
|
|
||||||
old_index = self._get_job_index(old_timestamp, old_job.id)
|
|
||||||
new_timestamp = datetime_to_utc_timestamp(job.next_run_time)
|
|
||||||
if old_timestamp == new_timestamp:
|
|
||||||
self._jobs[old_index] = (job, new_timestamp)
|
|
||||||
else:
|
|
||||||
del self._jobs[old_index]
|
|
||||||
new_index = self._get_job_index(new_timestamp, job.id)
|
|
||||||
self._jobs.insert(new_index, (job, new_timestamp))
|
|
||||||
|
|
||||||
self._jobs_index[old_job.id] = (job, new_timestamp)
|
|
||||||
|
|
||||||
def remove_job(self, job_id):
|
|
||||||
job, timestamp = self._jobs_index.get(job_id, (None, None))
|
|
||||||
if job is None:
|
|
||||||
raise JobLookupError(job_id)
|
|
||||||
|
|
||||||
index = self._get_job_index(timestamp, job_id)
|
|
||||||
del self._jobs[index]
|
|
||||||
del self._jobs_index[job.id]
|
|
||||||
|
|
||||||
def remove_all_jobs(self):
|
|
||||||
self._jobs = []
|
|
||||||
self._jobs_index = {}
|
|
||||||
|
|
||||||
def shutdown(self):
|
|
||||||
self.remove_all_jobs()
|
|
||||||
|
|
||||||
def _get_job_index(self, timestamp, job_id):
|
|
||||||
"""
|
|
||||||
Returns the index of the given job, or if it's not found, the index where the job should be
|
|
||||||
inserted based on the given timestamp.
|
|
||||||
|
|
||||||
:type timestamp: int
|
|
||||||
:type job_id: str
|
|
||||||
|
|
||||||
"""
|
|
||||||
lo, hi = 0, len(self._jobs)
|
|
||||||
timestamp = float('inf') if timestamp is None else timestamp
|
|
||||||
while lo < hi:
|
|
||||||
mid = (lo + hi) // 2
|
|
||||||
mid_job, mid_timestamp = self._jobs[mid]
|
|
||||||
mid_timestamp = float('inf') if mid_timestamp is None else mid_timestamp
|
|
||||||
if mid_timestamp > timestamp:
|
|
||||||
hi = mid
|
|
||||||
elif mid_timestamp < timestamp:
|
|
||||||
lo = mid + 1
|
|
||||||
elif mid_job.id > job_id:
|
|
||||||
hi = mid
|
|
||||||
elif mid_job.id < job_id:
|
|
||||||
lo = mid + 1
|
|
||||||
else:
|
|
||||||
return mid
|
|
||||||
|
|
||||||
return lo
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
from __future__ import absolute_import
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
from apscheduler.jobstores.base import BaseJobStore, JobLookupError, ConflictingIdError
|
|
||||||
from apscheduler.util import maybe_ref, datetime_to_utc_timestamp, utc_timestamp_to_datetime
|
|
||||||
from apscheduler.job import Job
|
|
||||||
|
|
||||||
try:
|
|
||||||
import cPickle as pickle
|
|
||||||
except ImportError: # pragma: nocover
|
|
||||||
import pickle
|
|
||||||
|
|
||||||
try:
|
|
||||||
from bson.binary import Binary
|
|
||||||
from pymongo.errors import DuplicateKeyError
|
|
||||||
from pymongo import MongoClient, ASCENDING
|
|
||||||
except ImportError: # pragma: nocover
|
|
||||||
raise ImportError('MongoDBJobStore requires PyMongo installed')
|
|
||||||
|
|
||||||
|
|
||||||
class MongoDBJobStore(BaseJobStore):
|
|
||||||
"""
|
|
||||||
Stores jobs in a MongoDB database. Any leftover keyword arguments are directly passed to
|
|
||||||
pymongo's `MongoClient
|
|
||||||
<http://api.mongodb.org/python/current/api/pymongo/mongo_client.html#pymongo.mongo_client.MongoClient>`_.
|
|
||||||
|
|
||||||
Plugin alias: ``mongodb``
|
|
||||||
|
|
||||||
:param str database: database to store jobs in
|
|
||||||
:param str collection: collection to store jobs in
|
|
||||||
:param client: a :class:`~pymongo.mongo_client.MongoClient` instance to use instead of
|
|
||||||
providing connection arguments
|
|
||||||
:param int pickle_protocol: pickle protocol level to use (for serialization), defaults to the
|
|
||||||
highest available
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, database='apscheduler', collection='jobs', client=None,
|
|
||||||
pickle_protocol=pickle.HIGHEST_PROTOCOL, **connect_args):
|
|
||||||
super(MongoDBJobStore, self).__init__()
|
|
||||||
self.pickle_protocol = pickle_protocol
|
|
||||||
|
|
||||||
if not database:
|
|
||||||
raise ValueError('The "database" parameter must not be empty')
|
|
||||||
if not collection:
|
|
||||||
raise ValueError('The "collection" parameter must not be empty')
|
|
||||||
|
|
||||||
if client:
|
|
||||||
self.client = maybe_ref(client)
|
|
||||||
else:
|
|
||||||
connect_args.setdefault('w', 1)
|
|
||||||
self.client = MongoClient(**connect_args)
|
|
||||||
|
|
||||||
self.collection = self.client[database][collection]
|
|
||||||
|
|
||||||
def start(self, scheduler, alias):
|
|
||||||
super(MongoDBJobStore, self).start(scheduler, alias)
|
|
||||||
self.collection.ensure_index('next_run_time', sparse=True)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def connection(self):
|
|
||||||
warnings.warn('The "connection" member is deprecated -- use "client" instead',
|
|
||||||
DeprecationWarning)
|
|
||||||
return self.client
|
|
||||||
|
|
||||||
def lookup_job(self, job_id):
|
|
||||||
document = self.collection.find_one(job_id, ['job_state'])
|
|
||||||
return self._reconstitute_job(document['job_state']) if document else None
|
|
||||||
|
|
||||||
def get_due_jobs(self, now):
|
|
||||||
timestamp = datetime_to_utc_timestamp(now)
|
|
||||||
return self._get_jobs({'next_run_time': {'$lte': timestamp}})
|
|
||||||
|
|
||||||
def get_next_run_time(self):
|
|
||||||
document = self.collection.find_one({'next_run_time': {'$ne': None}},
|
|
||||||
projection=['next_run_time'],
|
|
||||||
sort=[('next_run_time', ASCENDING)])
|
|
||||||
return utc_timestamp_to_datetime(document['next_run_time']) if document else None
|
|
||||||
|
|
||||||
def get_all_jobs(self):
|
|
||||||
jobs = self._get_jobs({})
|
|
||||||
self._fix_paused_jobs_sorting(jobs)
|
|
||||||
return jobs
|
|
||||||
|
|
||||||
def add_job(self, job):
|
|
||||||
try:
|
|
||||||
self.collection.insert({
|
|
||||||
'_id': job.id,
|
|
||||||
'next_run_time': datetime_to_utc_timestamp(job.next_run_time),
|
|
||||||
'job_state': Binary(pickle.dumps(job.__getstate__(), self.pickle_protocol))
|
|
||||||
})
|
|
||||||
except DuplicateKeyError:
|
|
||||||
raise ConflictingIdError(job.id)
|
|
||||||
|
|
||||||
def update_job(self, job):
|
|
||||||
changes = {
|
|
||||||
'next_run_time': datetime_to_utc_timestamp(job.next_run_time),
|
|
||||||
'job_state': Binary(pickle.dumps(job.__getstate__(), self.pickle_protocol))
|
|
||||||
}
|
|
||||||
result = self.collection.update({'_id': job.id}, {'$set': changes})
|
|
||||||
if result and result['n'] == 0:
|
|
||||||
raise JobLookupError(job.id)
|
|
||||||
|
|
||||||
def remove_job(self, job_id):
|
|
||||||
result = self.collection.remove(job_id)
|
|
||||||
if result and result['n'] == 0:
|
|
||||||
raise JobLookupError(job_id)
|
|
||||||
|
|
||||||
def remove_all_jobs(self):
|
|
||||||
self.collection.remove()
|
|
||||||
|
|
||||||
def shutdown(self):
|
|
||||||
self.client.close()
|
|
||||||
|
|
||||||
def _reconstitute_job(self, job_state):
|
|
||||||
job_state = pickle.loads(job_state)
|
|
||||||
job = Job.__new__(Job)
|
|
||||||
job.__setstate__(job_state)
|
|
||||||
job._scheduler = self._scheduler
|
|
||||||
job._jobstore_alias = self._alias
|
|
||||||
return job
|
|
||||||
|
|
||||||
def _get_jobs(self, conditions):
|
|
||||||
jobs = []
|
|
||||||
failed_job_ids = []
|
|
||||||
for document in self.collection.find(conditions, ['_id', 'job_state'],
|
|
||||||
sort=[('next_run_time', ASCENDING)]):
|
|
||||||
try:
|
|
||||||
jobs.append(self._reconstitute_job(document['job_state']))
|
|
||||||
except BaseException:
|
|
||||||
self._logger.exception('Unable to restore job "%s" -- removing it',
|
|
||||||
document['_id'])
|
|
||||||
failed_job_ids.append(document['_id'])
|
|
||||||
|
|
||||||
# Remove all the jobs we failed to restore
|
|
||||||
if failed_job_ids:
|
|
||||||
self.collection.remove({'_id': {'$in': failed_job_ids}})
|
|
||||||
|
|
||||||
return jobs
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<%s (client=%s)>' % (self.__class__.__name__, self.client)
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
from __future__ import absolute_import
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from pytz import utc
|
|
||||||
import six
|
|
||||||
|
|
||||||
from apscheduler.jobstores.base import BaseJobStore, JobLookupError, ConflictingIdError
|
|
||||||
from apscheduler.util import datetime_to_utc_timestamp, utc_timestamp_to_datetime
|
|
||||||
from apscheduler.job import Job
|
|
||||||
|
|
||||||
try:
|
|
||||||
import cPickle as pickle
|
|
||||||
except ImportError: # pragma: nocover
|
|
||||||
import pickle
|
|
||||||
|
|
||||||
try:
|
|
||||||
from redis import Redis
|
|
||||||
except ImportError: # pragma: nocover
|
|
||||||
raise ImportError('RedisJobStore requires redis installed')
|
|
||||||
|
|
||||||
|
|
||||||
class RedisJobStore(BaseJobStore):
|
|
||||||
"""
|
|
||||||
Stores jobs in a Redis database. Any leftover keyword arguments are directly passed to redis's
|
|
||||||
:class:`~redis.StrictRedis`.
|
|
||||||
|
|
||||||
Plugin alias: ``redis``
|
|
||||||
|
|
||||||
:param int db: the database number to store jobs in
|
|
||||||
:param str jobs_key: key to store jobs in
|
|
||||||
:param str run_times_key: key to store the jobs' run times in
|
|
||||||
:param int pickle_protocol: pickle protocol level to use (for serialization), defaults to the
|
|
||||||
highest available
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, db=0, jobs_key='apscheduler.jobs', run_times_key='apscheduler.run_times',
|
|
||||||
pickle_protocol=pickle.HIGHEST_PROTOCOL, **connect_args):
|
|
||||||
super(RedisJobStore, self).__init__()
|
|
||||||
|
|
||||||
if db is None:
|
|
||||||
raise ValueError('The "db" parameter must not be empty')
|
|
||||||
if not jobs_key:
|
|
||||||
raise ValueError('The "jobs_key" parameter must not be empty')
|
|
||||||
if not run_times_key:
|
|
||||||
raise ValueError('The "run_times_key" parameter must not be empty')
|
|
||||||
|
|
||||||
self.pickle_protocol = pickle_protocol
|
|
||||||
self.jobs_key = jobs_key
|
|
||||||
self.run_times_key = run_times_key
|
|
||||||
self.redis = Redis(db=int(db), **connect_args)
|
|
||||||
|
|
||||||
def lookup_job(self, job_id):
|
|
||||||
job_state = self.redis.hget(self.jobs_key, job_id)
|
|
||||||
return self._reconstitute_job(job_state) if job_state else None
|
|
||||||
|
|
||||||
def get_due_jobs(self, now):
|
|
||||||
timestamp = datetime_to_utc_timestamp(now)
|
|
||||||
job_ids = self.redis.zrangebyscore(self.run_times_key, 0, timestamp)
|
|
||||||
if job_ids:
|
|
||||||
job_states = self.redis.hmget(self.jobs_key, *job_ids)
|
|
||||||
return self._reconstitute_jobs(six.moves.zip(job_ids, job_states))
|
|
||||||
return []
|
|
||||||
|
|
||||||
def get_next_run_time(self):
|
|
||||||
next_run_time = self.redis.zrange(self.run_times_key, 0, 0, withscores=True)
|
|
||||||
if next_run_time:
|
|
||||||
return utc_timestamp_to_datetime(next_run_time[0][1])
|
|
||||||
|
|
||||||
def get_all_jobs(self):
|
|
||||||
job_states = self.redis.hgetall(self.jobs_key)
|
|
||||||
jobs = self._reconstitute_jobs(six.iteritems(job_states))
|
|
||||||
paused_sort_key = datetime(9999, 12, 31, tzinfo=utc)
|
|
||||||
return sorted(jobs, key=lambda job: job.next_run_time or paused_sort_key)
|
|
||||||
|
|
||||||
def add_job(self, job):
|
|
||||||
if self.redis.hexists(self.jobs_key, job.id):
|
|
||||||
raise ConflictingIdError(job.id)
|
|
||||||
|
|
||||||
with self.redis.pipeline() as pipe:
|
|
||||||
pipe.multi()
|
|
||||||
pipe.hset(self.jobs_key, job.id, pickle.dumps(job.__getstate__(),
|
|
||||||
self.pickle_protocol))
|
|
||||||
if job.next_run_time:
|
|
||||||
pipe.zadd(self.run_times_key,
|
|
||||||
{job.id: datetime_to_utc_timestamp(job.next_run_time)})
|
|
||||||
|
|
||||||
pipe.execute()
|
|
||||||
|
|
||||||
def update_job(self, job):
|
|
||||||
if not self.redis.hexists(self.jobs_key, job.id):
|
|
||||||
raise JobLookupError(job.id)
|
|
||||||
|
|
||||||
with self.redis.pipeline() as pipe:
|
|
||||||
pipe.hset(self.jobs_key, job.id, pickle.dumps(job.__getstate__(),
|
|
||||||
self.pickle_protocol))
|
|
||||||
if job.next_run_time:
|
|
||||||
pipe.zadd(self.run_times_key,
|
|
||||||
{job.id: datetime_to_utc_timestamp(job.next_run_time)})
|
|
||||||
else:
|
|
||||||
pipe.zrem(self.run_times_key, job.id)
|
|
||||||
|
|
||||||
pipe.execute()
|
|
||||||
|
|
||||||
def remove_job(self, job_id):
|
|
||||||
if not self.redis.hexists(self.jobs_key, job_id):
|
|
||||||
raise JobLookupError(job_id)
|
|
||||||
|
|
||||||
with self.redis.pipeline() as pipe:
|
|
||||||
pipe.hdel(self.jobs_key, job_id)
|
|
||||||
pipe.zrem(self.run_times_key, job_id)
|
|
||||||
pipe.execute()
|
|
||||||
|
|
||||||
def remove_all_jobs(self):
|
|
||||||
with self.redis.pipeline() as pipe:
|
|
||||||
pipe.delete(self.jobs_key)
|
|
||||||
pipe.delete(self.run_times_key)
|
|
||||||
pipe.execute()
|
|
||||||
|
|
||||||
def shutdown(self):
|
|
||||||
self.redis.connection_pool.disconnect()
|
|
||||||
|
|
||||||
def _reconstitute_job(self, job_state):
|
|
||||||
job_state = pickle.loads(job_state)
|
|
||||||
job = Job.__new__(Job)
|
|
||||||
job.__setstate__(job_state)
|
|
||||||
job._scheduler = self._scheduler
|
|
||||||
job._jobstore_alias = self._alias
|
|
||||||
return job
|
|
||||||
|
|
||||||
def _reconstitute_jobs(self, job_states):
|
|
||||||
jobs = []
|
|
||||||
failed_job_ids = []
|
|
||||||
for job_id, job_state in job_states:
|
|
||||||
try:
|
|
||||||
jobs.append(self._reconstitute_job(job_state))
|
|
||||||
except BaseException:
|
|
||||||
self._logger.exception('Unable to restore job "%s" -- removing it', job_id)
|
|
||||||
failed_job_ids.append(job_id)
|
|
||||||
|
|
||||||
# Remove all the jobs we failed to restore
|
|
||||||
if failed_job_ids:
|
|
||||||
with self.redis.pipeline() as pipe:
|
|
||||||
pipe.hdel(self.jobs_key, *failed_job_ids)
|
|
||||||
pipe.zrem(self.run_times_key, *failed_job_ids)
|
|
||||||
pipe.execute()
|
|
||||||
|
|
||||||
return jobs
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<%s>' % self.__class__.__name__
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
from apscheduler.jobstores.base import BaseJobStore, JobLookupError, ConflictingIdError
|
|
||||||
from apscheduler.util import maybe_ref, datetime_to_utc_timestamp, utc_timestamp_to_datetime
|
|
||||||
from apscheduler.job import Job
|
|
||||||
|
|
||||||
try:
|
|
||||||
import cPickle as pickle
|
|
||||||
except ImportError: # pragma: nocover
|
|
||||||
import pickle
|
|
||||||
|
|
||||||
try:
|
|
||||||
from rethinkdb import RethinkDB
|
|
||||||
except ImportError: # pragma: nocover
|
|
||||||
raise ImportError('RethinkDBJobStore requires rethinkdb installed')
|
|
||||||
|
|
||||||
|
|
||||||
class RethinkDBJobStore(BaseJobStore):
|
|
||||||
"""
|
|
||||||
Stores jobs in a RethinkDB database. Any leftover keyword arguments are directly passed to
|
|
||||||
rethinkdb's `RethinkdbClient <http://www.rethinkdb.com/api/#connect>`_.
|
|
||||||
|
|
||||||
Plugin alias: ``rethinkdb``
|
|
||||||
|
|
||||||
:param str database: database to store jobs in
|
|
||||||
:param str collection: collection to store jobs in
|
|
||||||
:param client: a :class:`rethinkdb.net.Connection` instance to use instead of providing
|
|
||||||
connection arguments
|
|
||||||
:param int pickle_protocol: pickle protocol level to use (for serialization), defaults to the
|
|
||||||
highest available
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, database='apscheduler', table='jobs', client=None,
|
|
||||||
pickle_protocol=pickle.HIGHEST_PROTOCOL, **connect_args):
|
|
||||||
super(RethinkDBJobStore, self).__init__()
|
|
||||||
|
|
||||||
if not database:
|
|
||||||
raise ValueError('The "database" parameter must not be empty')
|
|
||||||
if not table:
|
|
||||||
raise ValueError('The "table" parameter must not be empty')
|
|
||||||
|
|
||||||
self.database = database
|
|
||||||
self.table_name = table
|
|
||||||
self.table = None
|
|
||||||
self.client = client
|
|
||||||
self.pickle_protocol = pickle_protocol
|
|
||||||
self.connect_args = connect_args
|
|
||||||
self.r = RethinkDB()
|
|
||||||
self.conn = None
|
|
||||||
|
|
||||||
def start(self, scheduler, alias):
|
|
||||||
super(RethinkDBJobStore, self).start(scheduler, alias)
|
|
||||||
|
|
||||||
if self.client:
|
|
||||||
self.conn = maybe_ref(self.client)
|
|
||||||
else:
|
|
||||||
self.conn = self.r.connect(db=self.database, **self.connect_args)
|
|
||||||
|
|
||||||
if self.database not in self.r.db_list().run(self.conn):
|
|
||||||
self.r.db_create(self.database).run(self.conn)
|
|
||||||
|
|
||||||
if self.table_name not in self.r.table_list().run(self.conn):
|
|
||||||
self.r.table_create(self.table_name).run(self.conn)
|
|
||||||
|
|
||||||
if 'next_run_time' not in self.r.table(self.table_name).index_list().run(self.conn):
|
|
||||||
self.r.table(self.table_name).index_create('next_run_time').run(self.conn)
|
|
||||||
|
|
||||||
self.table = self.r.db(self.database).table(self.table_name)
|
|
||||||
|
|
||||||
def lookup_job(self, job_id):
|
|
||||||
results = list(self.table.get_all(job_id).pluck('job_state').run(self.conn))
|
|
||||||
return self._reconstitute_job(results[0]['job_state']) if results else None
|
|
||||||
|
|
||||||
def get_due_jobs(self, now):
|
|
||||||
return self._get_jobs(self.r.row['next_run_time'] <= datetime_to_utc_timestamp(now))
|
|
||||||
|
|
||||||
def get_next_run_time(self):
|
|
||||||
results = list(
|
|
||||||
self.table
|
|
||||||
.filter(self.r.row['next_run_time'] != None) # noqa
|
|
||||||
.order_by(self.r.asc('next_run_time'))
|
|
||||||
.map(lambda x: x['next_run_time'])
|
|
||||||
.limit(1)
|
|
||||||
.run(self.conn)
|
|
||||||
)
|
|
||||||
return utc_timestamp_to_datetime(results[0]) if results else None
|
|
||||||
|
|
||||||
def get_all_jobs(self):
|
|
||||||
jobs = self._get_jobs()
|
|
||||||
self._fix_paused_jobs_sorting(jobs)
|
|
||||||
return jobs
|
|
||||||
|
|
||||||
def add_job(self, job):
|
|
||||||
job_dict = {
|
|
||||||
'id': job.id,
|
|
||||||
'next_run_time': datetime_to_utc_timestamp(job.next_run_time),
|
|
||||||
'job_state': self.r.binary(pickle.dumps(job.__getstate__(), self.pickle_protocol))
|
|
||||||
}
|
|
||||||
results = self.table.insert(job_dict).run(self.conn)
|
|
||||||
if results['errors'] > 0:
|
|
||||||
raise ConflictingIdError(job.id)
|
|
||||||
|
|
||||||
def update_job(self, job):
|
|
||||||
changes = {
|
|
||||||
'next_run_time': datetime_to_utc_timestamp(job.next_run_time),
|
|
||||||
'job_state': self.r.binary(pickle.dumps(job.__getstate__(), self.pickle_protocol))
|
|
||||||
}
|
|
||||||
results = self.table.get_all(job.id).update(changes).run(self.conn)
|
|
||||||
skipped = False in map(lambda x: results[x] == 0, results.keys())
|
|
||||||
if results['skipped'] > 0 or results['errors'] > 0 or not skipped:
|
|
||||||
raise JobLookupError(job.id)
|
|
||||||
|
|
||||||
def remove_job(self, job_id):
|
|
||||||
results = self.table.get_all(job_id).delete().run(self.conn)
|
|
||||||
if results['deleted'] + results['skipped'] != 1:
|
|
||||||
raise JobLookupError(job_id)
|
|
||||||
|
|
||||||
def remove_all_jobs(self):
|
|
||||||
self.table.delete().run(self.conn)
|
|
||||||
|
|
||||||
def shutdown(self):
|
|
||||||
self.conn.close()
|
|
||||||
|
|
||||||
def _reconstitute_job(self, job_state):
|
|
||||||
job_state = pickle.loads(job_state)
|
|
||||||
job = Job.__new__(Job)
|
|
||||||
job.__setstate__(job_state)
|
|
||||||
job._scheduler = self._scheduler
|
|
||||||
job._jobstore_alias = self._alias
|
|
||||||
return job
|
|
||||||
|
|
||||||
def _get_jobs(self, predicate=None):
|
|
||||||
jobs = []
|
|
||||||
failed_job_ids = []
|
|
||||||
query = (self.table.filter(self.r.row['next_run_time'] != None).filter(predicate) # noqa
|
|
||||||
if predicate else self.table)
|
|
||||||
query = query.order_by('next_run_time', 'id').pluck('id', 'job_state')
|
|
||||||
|
|
||||||
for document in query.run(self.conn):
|
|
||||||
try:
|
|
||||||
jobs.append(self._reconstitute_job(document['job_state']))
|
|
||||||
except Exception:
|
|
||||||
self._logger.exception('Unable to restore job "%s" -- removing it', document['id'])
|
|
||||||
failed_job_ids.append(document['id'])
|
|
||||||
|
|
||||||
# Remove all the jobs we failed to restore
|
|
||||||
if failed_job_ids:
|
|
||||||
self.r.expr(failed_job_ids).for_each(
|
|
||||||
lambda job_id: self.table.get_all(job_id).delete()).run(self.conn)
|
|
||||||
|
|
||||||
return jobs
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
connection = self.conn
|
|
||||||
return '<%s (connection=%s)>' % (self.__class__.__name__, connection)
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
from apscheduler.jobstores.base import BaseJobStore, JobLookupError, ConflictingIdError
|
|
||||||
from apscheduler.util import maybe_ref, datetime_to_utc_timestamp, utc_timestamp_to_datetime
|
|
||||||
from apscheduler.job import Job
|
|
||||||
|
|
||||||
try:
|
|
||||||
import cPickle as pickle
|
|
||||||
except ImportError: # pragma: nocover
|
|
||||||
import pickle
|
|
||||||
|
|
||||||
try:
|
|
||||||
from sqlalchemy import (
|
|
||||||
create_engine, Table, Column, MetaData, Unicode, Float, LargeBinary, select)
|
|
||||||
from sqlalchemy.exc import IntegrityError
|
|
||||||
from sqlalchemy.sql.expression import null
|
|
||||||
except ImportError: # pragma: nocover
|
|
||||||
raise ImportError('SQLAlchemyJobStore requires SQLAlchemy installed')
|
|
||||||
|
|
||||||
|
|
||||||
class SQLAlchemyJobStore(BaseJobStore):
|
|
||||||
"""
|
|
||||||
Stores jobs in a database table using SQLAlchemy.
|
|
||||||
The table will be created if it doesn't exist in the database.
|
|
||||||
|
|
||||||
Plugin alias: ``sqlalchemy``
|
|
||||||
|
|
||||||
:param str url: connection string (see
|
|
||||||
:ref:`SQLAlchemy documentation <sqlalchemy:database_urls>` on this)
|
|
||||||
:param engine: an SQLAlchemy :class:`~sqlalchemy.engine.Engine` to use instead of creating a
|
|
||||||
new one based on ``url``
|
|
||||||
:param str tablename: name of the table to store jobs in
|
|
||||||
:param metadata: a :class:`~sqlalchemy.schema.MetaData` instance to use instead of creating a
|
|
||||||
new one
|
|
||||||
:param int pickle_protocol: pickle protocol level to use (for serialization), defaults to the
|
|
||||||
highest available
|
|
||||||
:param str tableschema: name of the (existing) schema in the target database where the table
|
|
||||||
should be
|
|
||||||
:param dict engine_options: keyword arguments to :func:`~sqlalchemy.create_engine`
|
|
||||||
(ignored if ``engine`` is given)
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, url=None, engine=None, tablename='apscheduler_jobs', metadata=None,
|
|
||||||
pickle_protocol=pickle.HIGHEST_PROTOCOL, tableschema=None, engine_options=None):
|
|
||||||
super(SQLAlchemyJobStore, self).__init__()
|
|
||||||
self.pickle_protocol = pickle_protocol
|
|
||||||
metadata = maybe_ref(metadata) or MetaData()
|
|
||||||
|
|
||||||
if engine:
|
|
||||||
self.engine = maybe_ref(engine)
|
|
||||||
elif url:
|
|
||||||
self.engine = create_engine(url, **(engine_options or {}))
|
|
||||||
else:
|
|
||||||
raise ValueError('Need either "engine" or "url" defined')
|
|
||||||
|
|
||||||
# 191 = max key length in MySQL for InnoDB/utf8mb4 tables,
|
|
||||||
# 25 = precision that translates to an 8-byte float
|
|
||||||
self.jobs_t = Table(
|
|
||||||
tablename, metadata,
|
|
||||||
Column('id', Unicode(191, _warn_on_bytestring=False), primary_key=True),
|
|
||||||
Column('next_run_time', Float(25), index=True),
|
|
||||||
Column('job_state', LargeBinary, nullable=False),
|
|
||||||
schema=tableschema
|
|
||||||
)
|
|
||||||
|
|
||||||
def start(self, scheduler, alias):
|
|
||||||
super(SQLAlchemyJobStore, self).start(scheduler, alias)
|
|
||||||
self.jobs_t.create(self.engine, True)
|
|
||||||
|
|
||||||
def lookup_job(self, job_id):
|
|
||||||
selectable = select([self.jobs_t.c.job_state]).where(self.jobs_t.c.id == job_id)
|
|
||||||
job_state = self.engine.execute(selectable).scalar()
|
|
||||||
return self._reconstitute_job(job_state) if job_state else None
|
|
||||||
|
|
||||||
def get_due_jobs(self, now):
|
|
||||||
timestamp = datetime_to_utc_timestamp(now)
|
|
||||||
return self._get_jobs(self.jobs_t.c.next_run_time <= timestamp)
|
|
||||||
|
|
||||||
def get_next_run_time(self):
|
|
||||||
selectable = select([self.jobs_t.c.next_run_time]).\
|
|
||||||
where(self.jobs_t.c.next_run_time != null()).\
|
|
||||||
order_by(self.jobs_t.c.next_run_time).limit(1)
|
|
||||||
next_run_time = self.engine.execute(selectable).scalar()
|
|
||||||
return utc_timestamp_to_datetime(next_run_time)
|
|
||||||
|
|
||||||
def get_all_jobs(self):
|
|
||||||
jobs = self._get_jobs()
|
|
||||||
self._fix_paused_jobs_sorting(jobs)
|
|
||||||
return jobs
|
|
||||||
|
|
||||||
def add_job(self, job):
|
|
||||||
insert = self.jobs_t.insert().values(**{
|
|
||||||
'id': job.id,
|
|
||||||
'next_run_time': datetime_to_utc_timestamp(job.next_run_time),
|
|
||||||
'job_state': pickle.dumps(job.__getstate__(), self.pickle_protocol)
|
|
||||||
})
|
|
||||||
try:
|
|
||||||
self.engine.execute(insert)
|
|
||||||
except IntegrityError:
|
|
||||||
raise ConflictingIdError(job.id)
|
|
||||||
|
|
||||||
def update_job(self, job):
|
|
||||||
update = self.jobs_t.update().values(**{
|
|
||||||
'next_run_time': datetime_to_utc_timestamp(job.next_run_time),
|
|
||||||
'job_state': pickle.dumps(job.__getstate__(), self.pickle_protocol)
|
|
||||||
}).where(self.jobs_t.c.id == job.id)
|
|
||||||
result = self.engine.execute(update)
|
|
||||||
if result.rowcount == 0:
|
|
||||||
raise JobLookupError(job.id)
|
|
||||||
|
|
||||||
def remove_job(self, job_id):
|
|
||||||
delete = self.jobs_t.delete().where(self.jobs_t.c.id == job_id)
|
|
||||||
result = self.engine.execute(delete)
|
|
||||||
if result.rowcount == 0:
|
|
||||||
raise JobLookupError(job_id)
|
|
||||||
|
|
||||||
def remove_all_jobs(self):
|
|
||||||
delete = self.jobs_t.delete()
|
|
||||||
self.engine.execute(delete)
|
|
||||||
|
|
||||||
def shutdown(self):
|
|
||||||
self.engine.dispose()
|
|
||||||
|
|
||||||
def _reconstitute_job(self, job_state):
|
|
||||||
job_state = pickle.loads(job_state)
|
|
||||||
job_state['jobstore'] = self
|
|
||||||
job = Job.__new__(Job)
|
|
||||||
job.__setstate__(job_state)
|
|
||||||
job._scheduler = self._scheduler
|
|
||||||
job._jobstore_alias = self._alias
|
|
||||||
return job
|
|
||||||
|
|
||||||
def _get_jobs(self, *conditions):
|
|
||||||
jobs = []
|
|
||||||
selectable = select([self.jobs_t.c.id, self.jobs_t.c.job_state]).\
|
|
||||||
order_by(self.jobs_t.c.next_run_time)
|
|
||||||
selectable = selectable.where(*conditions) if conditions else selectable
|
|
||||||
failed_job_ids = set()
|
|
||||||
for row in self.engine.execute(selectable):
|
|
||||||
try:
|
|
||||||
jobs.append(self._reconstitute_job(row.job_state))
|
|
||||||
except BaseException:
|
|
||||||
self._logger.exception('Unable to restore job "%s" -- removing it', row.id)
|
|
||||||
failed_job_ids.add(row.id)
|
|
||||||
|
|
||||||
# Remove all the jobs we failed to restore
|
|
||||||
if failed_job_ids:
|
|
||||||
delete = self.jobs_t.delete().where(self.jobs_t.c.id.in_(failed_job_ids))
|
|
||||||
self.engine.execute(delete)
|
|
||||||
|
|
||||||
return jobs
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<%s (url=%s)>' % (self.__class__.__name__, self.engine.url)
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
import os
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from pytz import utc
|
|
||||||
from kazoo.exceptions import NoNodeError, NodeExistsError
|
|
||||||
|
|
||||||
from apscheduler.jobstores.base import BaseJobStore, JobLookupError, ConflictingIdError
|
|
||||||
from apscheduler.util import maybe_ref, datetime_to_utc_timestamp, utc_timestamp_to_datetime
|
|
||||||
from apscheduler.job import Job
|
|
||||||
|
|
||||||
try:
|
|
||||||
import cPickle as pickle
|
|
||||||
except ImportError: # pragma: nocover
|
|
||||||
import pickle
|
|
||||||
|
|
||||||
try:
|
|
||||||
from kazoo.client import KazooClient
|
|
||||||
except ImportError: # pragma: nocover
|
|
||||||
raise ImportError('ZooKeeperJobStore requires Kazoo installed')
|
|
||||||
|
|
||||||
|
|
||||||
class ZooKeeperJobStore(BaseJobStore):
|
|
||||||
"""
|
|
||||||
Stores jobs in a ZooKeeper tree. Any leftover keyword arguments are directly passed to
|
|
||||||
kazoo's `KazooClient
|
|
||||||
<http://kazoo.readthedocs.io/en/latest/api/client.html>`_.
|
|
||||||
|
|
||||||
Plugin alias: ``zookeeper``
|
|
||||||
|
|
||||||
:param str path: path to store jobs in
|
|
||||||
:param client: a :class:`~kazoo.client.KazooClient` instance to use instead of
|
|
||||||
providing connection arguments
|
|
||||||
:param int pickle_protocol: pickle protocol level to use (for serialization), defaults to the
|
|
||||||
highest available
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, path='/apscheduler', client=None, close_connection_on_exit=False,
|
|
||||||
pickle_protocol=pickle.HIGHEST_PROTOCOL, **connect_args):
|
|
||||||
super(ZooKeeperJobStore, self).__init__()
|
|
||||||
self.pickle_protocol = pickle_protocol
|
|
||||||
self.close_connection_on_exit = close_connection_on_exit
|
|
||||||
|
|
||||||
if not path:
|
|
||||||
raise ValueError('The "path" parameter must not be empty')
|
|
||||||
|
|
||||||
self.path = path
|
|
||||||
|
|
||||||
if client:
|
|
||||||
self.client = maybe_ref(client)
|
|
||||||
else:
|
|
||||||
self.client = KazooClient(**connect_args)
|
|
||||||
self._ensured_path = False
|
|
||||||
|
|
||||||
def _ensure_paths(self):
|
|
||||||
if not self._ensured_path:
|
|
||||||
self.client.ensure_path(self.path)
|
|
||||||
self._ensured_path = True
|
|
||||||
|
|
||||||
def start(self, scheduler, alias):
|
|
||||||
super(ZooKeeperJobStore, self).start(scheduler, alias)
|
|
||||||
if not self.client.connected:
|
|
||||||
self.client.start()
|
|
||||||
|
|
||||||
def lookup_job(self, job_id):
|
|
||||||
self._ensure_paths()
|
|
||||||
node_path = os.path.join(self.path, job_id)
|
|
||||||
try:
|
|
||||||
content, _ = self.client.get(node_path)
|
|
||||||
doc = pickle.loads(content)
|
|
||||||
job = self._reconstitute_job(doc['job_state'])
|
|
||||||
return job
|
|
||||||
except BaseException:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_due_jobs(self, now):
|
|
||||||
timestamp = datetime_to_utc_timestamp(now)
|
|
||||||
jobs = [job_def['job'] for job_def in self._get_jobs()
|
|
||||||
if job_def['next_run_time'] is not None and job_def['next_run_time'] <= timestamp]
|
|
||||||
return jobs
|
|
||||||
|
|
||||||
def get_next_run_time(self):
|
|
||||||
next_runs = [job_def['next_run_time'] for job_def in self._get_jobs()
|
|
||||||
if job_def['next_run_time'] is not None]
|
|
||||||
return utc_timestamp_to_datetime(min(next_runs)) if len(next_runs) > 0 else None
|
|
||||||
|
|
||||||
def get_all_jobs(self):
|
|
||||||
jobs = [job_def['job'] for job_def in self._get_jobs()]
|
|
||||||
self._fix_paused_jobs_sorting(jobs)
|
|
||||||
return jobs
|
|
||||||
|
|
||||||
def add_job(self, job):
|
|
||||||
self._ensure_paths()
|
|
||||||
node_path = os.path.join(self.path, str(job.id))
|
|
||||||
value = {
|
|
||||||
'next_run_time': datetime_to_utc_timestamp(job.next_run_time),
|
|
||||||
'job_state': job.__getstate__()
|
|
||||||
}
|
|
||||||
data = pickle.dumps(value, self.pickle_protocol)
|
|
||||||
try:
|
|
||||||
self.client.create(node_path, value=data)
|
|
||||||
except NodeExistsError:
|
|
||||||
raise ConflictingIdError(job.id)
|
|
||||||
|
|
||||||
def update_job(self, job):
|
|
||||||
self._ensure_paths()
|
|
||||||
node_path = os.path.join(self.path, str(job.id))
|
|
||||||
changes = {
|
|
||||||
'next_run_time': datetime_to_utc_timestamp(job.next_run_time),
|
|
||||||
'job_state': job.__getstate__()
|
|
||||||
}
|
|
||||||
data = pickle.dumps(changes, self.pickle_protocol)
|
|
||||||
try:
|
|
||||||
self.client.set(node_path, value=data)
|
|
||||||
except NoNodeError:
|
|
||||||
raise JobLookupError(job.id)
|
|
||||||
|
|
||||||
def remove_job(self, job_id):
|
|
||||||
self._ensure_paths()
|
|
||||||
node_path = os.path.join(self.path, str(job_id))
|
|
||||||
try:
|
|
||||||
self.client.delete(node_path)
|
|
||||||
except NoNodeError:
|
|
||||||
raise JobLookupError(job_id)
|
|
||||||
|
|
||||||
def remove_all_jobs(self):
|
|
||||||
try:
|
|
||||||
self.client.delete(self.path, recursive=True)
|
|
||||||
except NoNodeError:
|
|
||||||
pass
|
|
||||||
self._ensured_path = False
|
|
||||||
|
|
||||||
def shutdown(self):
|
|
||||||
if self.close_connection_on_exit:
|
|
||||||
self.client.stop()
|
|
||||||
self.client.close()
|
|
||||||
|
|
||||||
def _reconstitute_job(self, job_state):
|
|
||||||
job_state = job_state
|
|
||||||
job = Job.__new__(Job)
|
|
||||||
job.__setstate__(job_state)
|
|
||||||
job._scheduler = self._scheduler
|
|
||||||
job._jobstore_alias = self._alias
|
|
||||||
return job
|
|
||||||
|
|
||||||
def _get_jobs(self):
|
|
||||||
self._ensure_paths()
|
|
||||||
jobs = []
|
|
||||||
failed_job_ids = []
|
|
||||||
all_ids = self.client.get_children(self.path)
|
|
||||||
for node_name in all_ids:
|
|
||||||
try:
|
|
||||||
node_path = os.path.join(self.path, node_name)
|
|
||||||
content, _ = self.client.get(node_path)
|
|
||||||
doc = pickle.loads(content)
|
|
||||||
job_def = {
|
|
||||||
'job_id': node_name,
|
|
||||||
'next_run_time': doc['next_run_time'] if doc['next_run_time'] else None,
|
|
||||||
'job_state': doc['job_state'],
|
|
||||||
'job': self._reconstitute_job(doc['job_state']),
|
|
||||||
'creation_time': _.ctime
|
|
||||||
}
|
|
||||||
jobs.append(job_def)
|
|
||||||
except BaseException:
|
|
||||||
self._logger.exception('Unable to restore job "%s" -- removing it' % node_name)
|
|
||||||
failed_job_ids.append(node_name)
|
|
||||||
|
|
||||||
# Remove all the jobs we failed to restore
|
|
||||||
if failed_job_ids:
|
|
||||||
for failed_id in failed_job_ids:
|
|
||||||
self.remove_job(failed_id)
|
|
||||||
paused_sort_key = datetime(9999, 12, 31, tzinfo=utc)
|
|
||||||
return sorted(jobs, key=lambda job_def: (job_def['job'].next_run_time or paused_sort_key,
|
|
||||||
job_def['creation_time']))
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
self._logger.exception('<%s (client=%s)>' % (self.__class__.__name__, self.client))
|
|
||||||
return '<%s (client=%s)>' % (self.__class__.__name__, self.client)
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
class SchedulerAlreadyRunningError(Exception):
|
|
||||||
"""Raised when attempting to start or configure the scheduler when it's already running."""
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return 'Scheduler is already running'
|
|
||||||
|
|
||||||
|
|
||||||
class SchedulerNotRunningError(Exception):
|
|
||||||
"""Raised when attempting to shutdown the scheduler when it's not running."""
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return 'Scheduler is not running'
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
from __future__ import absolute_import
|
|
||||||
from functools import wraps, partial
|
|
||||||
|
|
||||||
from apscheduler.schedulers.base import BaseScheduler
|
|
||||||
from apscheduler.util import maybe_ref
|
|
||||||
|
|
||||||
try:
|
|
||||||
import asyncio
|
|
||||||
except ImportError: # pragma: nocover
|
|
||||||
try:
|
|
||||||
import trollius as asyncio
|
|
||||||
except ImportError:
|
|
||||||
raise ImportError(
|
|
||||||
'AsyncIOScheduler requires either Python 3.4 or the asyncio package installed')
|
|
||||||
|
|
||||||
|
|
||||||
def run_in_event_loop(func):
|
|
||||||
@wraps(func)
|
|
||||||
def wrapper(self, *args, **kwargs):
|
|
||||||
wrapped = partial(func, self, *args, **kwargs)
|
|
||||||
self._eventloop.call_soon_threadsafe(wrapped)
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
class AsyncIOScheduler(BaseScheduler):
|
|
||||||
"""
|
|
||||||
A scheduler that runs on an asyncio (:pep:`3156`) event loop.
|
|
||||||
|
|
||||||
The default executor can run jobs based on native coroutines (``async def``).
|
|
||||||
|
|
||||||
Extra options:
|
|
||||||
|
|
||||||
============== =============================================================
|
|
||||||
``event_loop`` AsyncIO event loop to use (defaults to the global event loop)
|
|
||||||
============== =============================================================
|
|
||||||
"""
|
|
||||||
|
|
||||||
_eventloop = None
|
|
||||||
_timeout = None
|
|
||||||
|
|
||||||
@run_in_event_loop
|
|
||||||
def shutdown(self, wait=True):
|
|
||||||
super(AsyncIOScheduler, self).shutdown(wait)
|
|
||||||
self._stop_timer()
|
|
||||||
|
|
||||||
def _configure(self, config):
|
|
||||||
self._eventloop = maybe_ref(config.pop('event_loop', None)) or asyncio.get_event_loop()
|
|
||||||
super(AsyncIOScheduler, self)._configure(config)
|
|
||||||
|
|
||||||
def _start_timer(self, wait_seconds):
|
|
||||||
self._stop_timer()
|
|
||||||
if wait_seconds is not None:
|
|
||||||
self._timeout = self._eventloop.call_later(wait_seconds, self.wakeup)
|
|
||||||
|
|
||||||
def _stop_timer(self):
|
|
||||||
if self._timeout:
|
|
||||||
self._timeout.cancel()
|
|
||||||
del self._timeout
|
|
||||||
|
|
||||||
@run_in_event_loop
|
|
||||||
def wakeup(self):
|
|
||||||
self._stop_timer()
|
|
||||||
wait_seconds = self._process_jobs()
|
|
||||||
self._start_timer(wait_seconds)
|
|
||||||
|
|
||||||
def _create_default_executor(self):
|
|
||||||
from apscheduler.executors.asyncio import AsyncIOExecutor
|
|
||||||
return AsyncIOExecutor()
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
from threading import Thread, Event
|
|
||||||
|
|
||||||
from apscheduler.schedulers.base import BaseScheduler
|
|
||||||
from apscheduler.schedulers.blocking import BlockingScheduler
|
|
||||||
from apscheduler.util import asbool
|
|
||||||
|
|
||||||
|
|
||||||
class BackgroundScheduler(BlockingScheduler):
|
|
||||||
"""
|
|
||||||
A scheduler that runs in the background using a separate thread
|
|
||||||
(:meth:`~apscheduler.schedulers.base.BaseScheduler.start` will return immediately).
|
|
||||||
|
|
||||||
Extra options:
|
|
||||||
|
|
||||||
========== =============================================================================
|
|
||||||
``daemon`` Set the ``daemon`` option in the background thread (defaults to ``True``, see
|
|
||||||
`the documentation
|
|
||||||
<https://docs.python.org/3.4/library/threading.html#thread-objects>`_
|
|
||||||
for further details)
|
|
||||||
========== =============================================================================
|
|
||||||
"""
|
|
||||||
|
|
||||||
_thread = None
|
|
||||||
|
|
||||||
def _configure(self, config):
|
|
||||||
self._daemon = asbool(config.pop('daemon', True))
|
|
||||||
super(BackgroundScheduler, self)._configure(config)
|
|
||||||
|
|
||||||
def start(self, *args, **kwargs):
|
|
||||||
self._event = Event()
|
|
||||||
BaseScheduler.start(self, *args, **kwargs)
|
|
||||||
self._thread = Thread(target=self._main_loop, name='APScheduler')
|
|
||||||
self._thread.daemon = self._daemon
|
|
||||||
self._thread.start()
|
|
||||||
|
|
||||||
def shutdown(self, *args, **kwargs):
|
|
||||||
super(BackgroundScheduler, self).shutdown(*args, **kwargs)
|
|
||||||
self._thread.join()
|
|
||||||
del self._thread
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,33 +0,0 @@
|
|||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
from threading import Event
|
|
||||||
|
|
||||||
from apscheduler.schedulers.base import BaseScheduler, STATE_STOPPED
|
|
||||||
from apscheduler.util import TIMEOUT_MAX
|
|
||||||
|
|
||||||
|
|
||||||
class BlockingScheduler(BaseScheduler):
|
|
||||||
"""
|
|
||||||
A scheduler that runs in the foreground
|
|
||||||
(:meth:`~apscheduler.schedulers.base.BaseScheduler.start` will block).
|
|
||||||
"""
|
|
||||||
_event = None
|
|
||||||
|
|
||||||
def start(self, *args, **kwargs):
|
|
||||||
self._event = Event()
|
|
||||||
super(BlockingScheduler, self).start(*args, **kwargs)
|
|
||||||
self._main_loop()
|
|
||||||
|
|
||||||
def shutdown(self, wait=True):
|
|
||||||
super(BlockingScheduler, self).shutdown(wait)
|
|
||||||
self._event.set()
|
|
||||||
|
|
||||||
def _main_loop(self):
|
|
||||||
wait_seconds = TIMEOUT_MAX
|
|
||||||
while self.state != STATE_STOPPED:
|
|
||||||
self._event.wait(wait_seconds)
|
|
||||||
self._event.clear()
|
|
||||||
wait_seconds = self._process_jobs()
|
|
||||||
|
|
||||||
def wakeup(self):
|
|
||||||
self._event.set()
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
from apscheduler.schedulers.blocking import BlockingScheduler
|
|
||||||
from apscheduler.schedulers.base import BaseScheduler
|
|
||||||
|
|
||||||
try:
|
|
||||||
from gevent.event import Event
|
|
||||||
from gevent.lock import RLock
|
|
||||||
import gevent
|
|
||||||
except ImportError: # pragma: nocover
|
|
||||||
raise ImportError('GeventScheduler requires gevent installed')
|
|
||||||
|
|
||||||
|
|
||||||
class GeventScheduler(BlockingScheduler):
|
|
||||||
"""A scheduler that runs as a Gevent greenlet."""
|
|
||||||
|
|
||||||
_greenlet = None
|
|
||||||
|
|
||||||
def start(self, *args, **kwargs):
|
|
||||||
self._event = Event()
|
|
||||||
BaseScheduler.start(self, *args, **kwargs)
|
|
||||||
self._greenlet = gevent.spawn(self._main_loop)
|
|
||||||
return self._greenlet
|
|
||||||
|
|
||||||
def shutdown(self, *args, **kwargs):
|
|
||||||
super(GeventScheduler, self).shutdown(*args, **kwargs)
|
|
||||||
self._greenlet.join()
|
|
||||||
del self._greenlet
|
|
||||||
|
|
||||||
def _create_lock(self):
|
|
||||||
return RLock()
|
|
||||||
|
|
||||||
def _create_default_executor(self):
|
|
||||||
from apscheduler.executors.gevent import GeventExecutor
|
|
||||||
return GeventExecutor()
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
from apscheduler.schedulers.base import BaseScheduler
|
|
||||||
|
|
||||||
try:
|
|
||||||
from PyQt5.QtCore import QObject, QTimer
|
|
||||||
except (ImportError, RuntimeError): # pragma: nocover
|
|
||||||
try:
|
|
||||||
from PyQt4.QtCore import QObject, QTimer
|
|
||||||
except ImportError:
|
|
||||||
try:
|
|
||||||
from PySide.QtCore import QObject, QTimer # noqa
|
|
||||||
except ImportError:
|
|
||||||
raise ImportError('QtScheduler requires either PyQt5, PyQt4 or PySide installed')
|
|
||||||
|
|
||||||
|
|
||||||
class QtScheduler(BaseScheduler):
|
|
||||||
"""A scheduler that runs in a Qt event loop."""
|
|
||||||
|
|
||||||
_timer = None
|
|
||||||
|
|
||||||
def shutdown(self, *args, **kwargs):
|
|
||||||
super(QtScheduler, self).shutdown(*args, **kwargs)
|
|
||||||
self._stop_timer()
|
|
||||||
|
|
||||||
def _start_timer(self, wait_seconds):
|
|
||||||
self._stop_timer()
|
|
||||||
if wait_seconds is not None:
|
|
||||||
wait_time = min(wait_seconds * 1000, 2147483647)
|
|
||||||
self._timer = QTimer.singleShot(wait_time, self._process_jobs)
|
|
||||||
|
|
||||||
def _stop_timer(self):
|
|
||||||
if self._timer:
|
|
||||||
if self._timer.isActive():
|
|
||||||
self._timer.stop()
|
|
||||||
del self._timer
|
|
||||||
|
|
||||||
def wakeup(self):
|
|
||||||
self._start_timer(0)
|
|
||||||
|
|
||||||
def _process_jobs(self):
|
|
||||||
wait_seconds = super(QtScheduler, self)._process_jobs()
|
|
||||||
self._start_timer(wait_seconds)
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
from datetime import timedelta
|
|
||||||
from functools import wraps
|
|
||||||
|
|
||||||
from apscheduler.schedulers.base import BaseScheduler
|
|
||||||
from apscheduler.util import maybe_ref
|
|
||||||
|
|
||||||
try:
|
|
||||||
from tornado.ioloop import IOLoop
|
|
||||||
except ImportError: # pragma: nocover
|
|
||||||
raise ImportError('TornadoScheduler requires tornado installed')
|
|
||||||
|
|
||||||
|
|
||||||
def run_in_ioloop(func):
|
|
||||||
@wraps(func)
|
|
||||||
def wrapper(self, *args, **kwargs):
|
|
||||||
self._ioloop.add_callback(func, self, *args, **kwargs)
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
class TornadoScheduler(BaseScheduler):
|
|
||||||
"""
|
|
||||||
A scheduler that runs on a Tornado IOLoop.
|
|
||||||
|
|
||||||
The default executor can run jobs based on native coroutines (``async def``).
|
|
||||||
|
|
||||||
=========== ===============================================================
|
|
||||||
``io_loop`` Tornado IOLoop instance to use (defaults to the global IO loop)
|
|
||||||
=========== ===============================================================
|
|
||||||
"""
|
|
||||||
|
|
||||||
_ioloop = None
|
|
||||||
_timeout = None
|
|
||||||
|
|
||||||
@run_in_ioloop
|
|
||||||
def shutdown(self, wait=True):
|
|
||||||
super(TornadoScheduler, self).shutdown(wait)
|
|
||||||
self._stop_timer()
|
|
||||||
|
|
||||||
def _configure(self, config):
|
|
||||||
self._ioloop = maybe_ref(config.pop('io_loop', None)) or IOLoop.current()
|
|
||||||
super(TornadoScheduler, self)._configure(config)
|
|
||||||
|
|
||||||
def _start_timer(self, wait_seconds):
|
|
||||||
self._stop_timer()
|
|
||||||
if wait_seconds is not None:
|
|
||||||
self._timeout = self._ioloop.add_timeout(timedelta(seconds=wait_seconds), self.wakeup)
|
|
||||||
|
|
||||||
def _stop_timer(self):
|
|
||||||
if self._timeout:
|
|
||||||
self._ioloop.remove_timeout(self._timeout)
|
|
||||||
del self._timeout
|
|
||||||
|
|
||||||
def _create_default_executor(self):
|
|
||||||
from apscheduler.executors.tornado import TornadoExecutor
|
|
||||||
return TornadoExecutor()
|
|
||||||
|
|
||||||
@run_in_ioloop
|
|
||||||
def wakeup(self):
|
|
||||||
self._stop_timer()
|
|
||||||
wait_seconds = self._process_jobs()
|
|
||||||
self._start_timer(wait_seconds)
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
from functools import wraps
|
|
||||||
|
|
||||||
from apscheduler.schedulers.base import BaseScheduler
|
|
||||||
from apscheduler.util import maybe_ref
|
|
||||||
|
|
||||||
try:
|
|
||||||
from twisted.internet import reactor as default_reactor
|
|
||||||
except ImportError: # pragma: nocover
|
|
||||||
raise ImportError('TwistedScheduler requires Twisted installed')
|
|
||||||
|
|
||||||
|
|
||||||
def run_in_reactor(func):
|
|
||||||
@wraps(func)
|
|
||||||
def wrapper(self, *args, **kwargs):
|
|
||||||
self._reactor.callFromThread(func, self, *args, **kwargs)
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
class TwistedScheduler(BaseScheduler):
|
|
||||||
"""
|
|
||||||
A scheduler that runs on a Twisted reactor.
|
|
||||||
|
|
||||||
Extra options:
|
|
||||||
|
|
||||||
=========== ========================================================
|
|
||||||
``reactor`` Reactor instance to use (defaults to the global reactor)
|
|
||||||
=========== ========================================================
|
|
||||||
"""
|
|
||||||
|
|
||||||
_reactor = None
|
|
||||||
_delayedcall = None
|
|
||||||
|
|
||||||
def _configure(self, config):
|
|
||||||
self._reactor = maybe_ref(config.pop('reactor', default_reactor))
|
|
||||||
super(TwistedScheduler, self)._configure(config)
|
|
||||||
|
|
||||||
@run_in_reactor
|
|
||||||
def shutdown(self, wait=True):
|
|
||||||
super(TwistedScheduler, self).shutdown(wait)
|
|
||||||
self._stop_timer()
|
|
||||||
|
|
||||||
def _start_timer(self, wait_seconds):
|
|
||||||
self._stop_timer()
|
|
||||||
if wait_seconds is not None:
|
|
||||||
self._delayedcall = self._reactor.callLater(wait_seconds, self.wakeup)
|
|
||||||
|
|
||||||
def _stop_timer(self):
|
|
||||||
if self._delayedcall and self._delayedcall.active():
|
|
||||||
self._delayedcall.cancel()
|
|
||||||
del self._delayedcall
|
|
||||||
|
|
||||||
@run_in_reactor
|
|
||||||
def wakeup(self):
|
|
||||||
self._stop_timer()
|
|
||||||
wait_seconds = self._process_jobs()
|
|
||||||
self._start_timer(wait_seconds)
|
|
||||||
|
|
||||||
def _create_default_executor(self):
|
|
||||||
from apscheduler.executors.twisted import TwistedExecutor
|
|
||||||
return TwistedExecutor()
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
from abc import ABCMeta, abstractmethod
|
|
||||||
from datetime import timedelta
|
|
||||||
import random
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
|
|
||||||
class BaseTrigger(six.with_metaclass(ABCMeta)):
|
|
||||||
"""Abstract base class that defines the interface that every trigger must implement."""
|
|
||||||
|
|
||||||
__slots__ = ()
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_next_fire_time(self, previous_fire_time, now):
|
|
||||||
"""
|
|
||||||
Returns the next datetime to fire on, If no such datetime can be calculated, returns
|
|
||||||
``None``.
|
|
||||||
|
|
||||||
:param datetime.datetime previous_fire_time: the previous time the trigger was fired
|
|
||||||
:param datetime.datetime now: current datetime
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _apply_jitter(self, next_fire_time, jitter, now):
|
|
||||||
"""
|
|
||||||
Randomize ``next_fire_time`` by adding or subtracting a random value (the jitter). If the
|
|
||||||
resulting datetime is in the past, returns the initial ``next_fire_time`` without jitter.
|
|
||||||
|
|
||||||
``next_fire_time - jitter <= result <= next_fire_time + jitter``
|
|
||||||
|
|
||||||
:param datetime.datetime|None next_fire_time: next fire time without jitter applied. If
|
|
||||||
``None``, returns ``None``.
|
|
||||||
:param int|None jitter: maximum number of seconds to add or subtract to
|
|
||||||
``next_fire_time``. If ``None`` or ``0``, returns ``next_fire_time``
|
|
||||||
:param datetime.datetime now: current datetime
|
|
||||||
:return datetime.datetime|None: next fire time with a jitter.
|
|
||||||
"""
|
|
||||||
if next_fire_time is None or not jitter:
|
|
||||||
return next_fire_time
|
|
||||||
|
|
||||||
next_fire_time_with_jitter = next_fire_time + timedelta(
|
|
||||||
seconds=random.uniform(-jitter, jitter))
|
|
||||||
|
|
||||||
if next_fire_time_with_jitter < now:
|
|
||||||
# Next fire time with jitter is in the past.
|
|
||||||
# Ignore jitter to avoid false misfire.
|
|
||||||
return next_fire_time
|
|
||||||
|
|
||||||
return next_fire_time_with_jitter
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
from apscheduler.triggers.base import BaseTrigger
|
|
||||||
from apscheduler.util import obj_to_ref, ref_to_obj
|
|
||||||
|
|
||||||
|
|
||||||
class BaseCombiningTrigger(BaseTrigger):
|
|
||||||
__slots__ = ('triggers', 'jitter')
|
|
||||||
|
|
||||||
def __init__(self, triggers, jitter=None):
|
|
||||||
self.triggers = triggers
|
|
||||||
self.jitter = jitter
|
|
||||||
|
|
||||||
def __getstate__(self):
|
|
||||||
return {
|
|
||||||
'version': 1,
|
|
||||||
'triggers': [(obj_to_ref(trigger.__class__), trigger.__getstate__())
|
|
||||||
for trigger in self.triggers],
|
|
||||||
'jitter': self.jitter
|
|
||||||
}
|
|
||||||
|
|
||||||
def __setstate__(self, state):
|
|
||||||
if state.get('version', 1) > 1:
|
|
||||||
raise ValueError(
|
|
||||||
'Got serialized data for version %s of %s, but only versions up to 1 can be '
|
|
||||||
'handled' % (state['version'], self.__class__.__name__))
|
|
||||||
|
|
||||||
self.jitter = state['jitter']
|
|
||||||
self.triggers = []
|
|
||||||
for clsref, state in state['triggers']:
|
|
||||||
cls = ref_to_obj(clsref)
|
|
||||||
trigger = cls.__new__(cls)
|
|
||||||
trigger.__setstate__(state)
|
|
||||||
self.triggers.append(trigger)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<{}({}{})>'.format(self.__class__.__name__, self.triggers,
|
|
||||||
', jitter={}'.format(self.jitter) if self.jitter else '')
|
|
||||||
|
|
||||||
|
|
||||||
class AndTrigger(BaseCombiningTrigger):
|
|
||||||
"""
|
|
||||||
Always returns the earliest next fire time that all the given triggers can agree on.
|
|
||||||
The trigger is considered to be finished when any of the given triggers has finished its
|
|
||||||
schedule.
|
|
||||||
|
|
||||||
Trigger alias: ``and``
|
|
||||||
|
|
||||||
:param list triggers: triggers to combine
|
|
||||||
:param int|None jitter: advance or delay the job execution by ``jitter`` seconds at most.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ()
|
|
||||||
|
|
||||||
def get_next_fire_time(self, previous_fire_time, now):
|
|
||||||
while True:
|
|
||||||
fire_times = [trigger.get_next_fire_time(previous_fire_time, now)
|
|
||||||
for trigger in self.triggers]
|
|
||||||
if None in fire_times:
|
|
||||||
return None
|
|
||||||
elif min(fire_times) == max(fire_times):
|
|
||||||
return self._apply_jitter(fire_times[0], self.jitter, now)
|
|
||||||
else:
|
|
||||||
now = max(fire_times)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return 'and[{}]'.format(', '.join(str(trigger) for trigger in self.triggers))
|
|
||||||
|
|
||||||
|
|
||||||
class OrTrigger(BaseCombiningTrigger):
|
|
||||||
"""
|
|
||||||
Always returns the earliest next fire time produced by any of the given triggers.
|
|
||||||
The trigger is considered finished when all the given triggers have finished their schedules.
|
|
||||||
|
|
||||||
Trigger alias: ``or``
|
|
||||||
|
|
||||||
:param list triggers: triggers to combine
|
|
||||||
:param int|None jitter: advance or delay the job execution by ``jitter`` seconds at most.
|
|
||||||
|
|
||||||
.. note:: Triggers that depends on the previous fire time, such as the interval trigger, may
|
|
||||||
seem to behave strangely since they are always passed the previous fire time produced by
|
|
||||||
any of the given triggers.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ()
|
|
||||||
|
|
||||||
def get_next_fire_time(self, previous_fire_time, now):
|
|
||||||
fire_times = [trigger.get_next_fire_time(previous_fire_time, now)
|
|
||||||
for trigger in self.triggers]
|
|
||||||
fire_times = [fire_time for fire_time in fire_times if fire_time is not None]
|
|
||||||
if fire_times:
|
|
||||||
return self._apply_jitter(min(fire_times), self.jitter, now)
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return 'or[{}]'.format(', '.join(str(trigger) for trigger in self.triggers))
|
|
||||||
@@ -1,238 +0,0 @@
|
|||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
from tzlocal import get_localzone
|
|
||||||
import six
|
|
||||||
|
|
||||||
from apscheduler.triggers.base import BaseTrigger
|
|
||||||
from apscheduler.triggers.cron.fields import (
|
|
||||||
BaseField, MonthField, WeekField, DayOfMonthField, DayOfWeekField, DEFAULT_VALUES)
|
|
||||||
from apscheduler.util import datetime_ceil, convert_to_datetime, datetime_repr, astimezone
|
|
||||||
|
|
||||||
|
|
||||||
class CronTrigger(BaseTrigger):
|
|
||||||
"""
|
|
||||||
Triggers when current time matches all specified time constraints,
|
|
||||||
similarly to how the UNIX cron scheduler works.
|
|
||||||
|
|
||||||
:param int|str year: 4-digit year
|
|
||||||
:param int|str month: month (1-12)
|
|
||||||
:param int|str day: day of the (1-31)
|
|
||||||
:param int|str week: ISO week (1-53)
|
|
||||||
:param int|str day_of_week: number or name of weekday (0-6 or mon,tue,wed,thu,fri,sat,sun)
|
|
||||||
:param int|str hour: hour (0-23)
|
|
||||||
:param int|str minute: minute (0-59)
|
|
||||||
:param int|str second: second (0-59)
|
|
||||||
:param datetime|str start_date: earliest possible date/time to trigger on (inclusive)
|
|
||||||
:param datetime|str end_date: latest possible date/time to trigger on (inclusive)
|
|
||||||
:param datetime.tzinfo|str timezone: time zone to use for the date/time calculations (defaults
|
|
||||||
to scheduler timezone)
|
|
||||||
:param int|None jitter: advance or delay the job execution by ``jitter`` seconds at most.
|
|
||||||
|
|
||||||
.. note:: The first weekday is always **monday**.
|
|
||||||
"""
|
|
||||||
|
|
||||||
FIELD_NAMES = ('year', 'month', 'day', 'week', 'day_of_week', 'hour', 'minute', 'second')
|
|
||||||
FIELDS_MAP = {
|
|
||||||
'year': BaseField,
|
|
||||||
'month': MonthField,
|
|
||||||
'week': WeekField,
|
|
||||||
'day': DayOfMonthField,
|
|
||||||
'day_of_week': DayOfWeekField,
|
|
||||||
'hour': BaseField,
|
|
||||||
'minute': BaseField,
|
|
||||||
'second': BaseField
|
|
||||||
}
|
|
||||||
|
|
||||||
__slots__ = 'timezone', 'start_date', 'end_date', 'fields', 'jitter'
|
|
||||||
|
|
||||||
def __init__(self, year=None, month=None, day=None, week=None, day_of_week=None, hour=None,
|
|
||||||
minute=None, second=None, start_date=None, end_date=None, timezone=None,
|
|
||||||
jitter=None):
|
|
||||||
if timezone:
|
|
||||||
self.timezone = astimezone(timezone)
|
|
||||||
elif isinstance(start_date, datetime) and start_date.tzinfo:
|
|
||||||
self.timezone = start_date.tzinfo
|
|
||||||
elif isinstance(end_date, datetime) and end_date.tzinfo:
|
|
||||||
self.timezone = end_date.tzinfo
|
|
||||||
else:
|
|
||||||
self.timezone = get_localzone()
|
|
||||||
|
|
||||||
self.start_date = convert_to_datetime(start_date, self.timezone, 'start_date')
|
|
||||||
self.end_date = convert_to_datetime(end_date, self.timezone, 'end_date')
|
|
||||||
|
|
||||||
self.jitter = jitter
|
|
||||||
|
|
||||||
values = dict((key, value) for (key, value) in six.iteritems(locals())
|
|
||||||
if key in self.FIELD_NAMES and value is not None)
|
|
||||||
self.fields = []
|
|
||||||
assign_defaults = False
|
|
||||||
for field_name in self.FIELD_NAMES:
|
|
||||||
if field_name in values:
|
|
||||||
exprs = values.pop(field_name)
|
|
||||||
is_default = False
|
|
||||||
assign_defaults = not values
|
|
||||||
elif assign_defaults:
|
|
||||||
exprs = DEFAULT_VALUES[field_name]
|
|
||||||
is_default = True
|
|
||||||
else:
|
|
||||||
exprs = '*'
|
|
||||||
is_default = True
|
|
||||||
|
|
||||||
field_class = self.FIELDS_MAP[field_name]
|
|
||||||
field = field_class(field_name, exprs, is_default)
|
|
||||||
self.fields.append(field)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_crontab(cls, expr, timezone=None):
|
|
||||||
"""
|
|
||||||
Create a :class:`~CronTrigger` from a standard crontab expression.
|
|
||||||
|
|
||||||
See https://en.wikipedia.org/wiki/Cron for more information on the format accepted here.
|
|
||||||
|
|
||||||
:param expr: minute, hour, day of month, month, day of week
|
|
||||||
:param datetime.tzinfo|str timezone: time zone to use for the date/time calculations (
|
|
||||||
defaults to scheduler timezone)
|
|
||||||
:return: a :class:`~CronTrigger` instance
|
|
||||||
|
|
||||||
"""
|
|
||||||
values = expr.split()
|
|
||||||
if len(values) != 5:
|
|
||||||
raise ValueError('Wrong number of fields; got {}, expected 5'.format(len(values)))
|
|
||||||
|
|
||||||
return cls(minute=values[0], hour=values[1], day=values[2], month=values[3],
|
|
||||||
day_of_week=values[4], timezone=timezone)
|
|
||||||
|
|
||||||
def _increment_field_value(self, dateval, fieldnum):
|
|
||||||
"""
|
|
||||||
Increments the designated field and resets all less significant fields to their minimum
|
|
||||||
values.
|
|
||||||
|
|
||||||
:type dateval: datetime
|
|
||||||
:type fieldnum: int
|
|
||||||
:return: a tuple containing the new date, and the number of the field that was actually
|
|
||||||
incremented
|
|
||||||
:rtype: tuple
|
|
||||||
"""
|
|
||||||
|
|
||||||
values = {}
|
|
||||||
i = 0
|
|
||||||
while i < len(self.fields):
|
|
||||||
field = self.fields[i]
|
|
||||||
if not field.REAL:
|
|
||||||
if i == fieldnum:
|
|
||||||
fieldnum -= 1
|
|
||||||
i -= 1
|
|
||||||
else:
|
|
||||||
i += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
if i < fieldnum:
|
|
||||||
values[field.name] = field.get_value(dateval)
|
|
||||||
i += 1
|
|
||||||
elif i > fieldnum:
|
|
||||||
values[field.name] = field.get_min(dateval)
|
|
||||||
i += 1
|
|
||||||
else:
|
|
||||||
value = field.get_value(dateval)
|
|
||||||
maxval = field.get_max(dateval)
|
|
||||||
if value == maxval:
|
|
||||||
fieldnum -= 1
|
|
||||||
i -= 1
|
|
||||||
else:
|
|
||||||
values[field.name] = value + 1
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
difference = datetime(**values) - dateval.replace(tzinfo=None)
|
|
||||||
return self.timezone.normalize(dateval + difference), fieldnum
|
|
||||||
|
|
||||||
def _set_field_value(self, dateval, fieldnum, new_value):
|
|
||||||
values = {}
|
|
||||||
for i, field in enumerate(self.fields):
|
|
||||||
if field.REAL:
|
|
||||||
if i < fieldnum:
|
|
||||||
values[field.name] = field.get_value(dateval)
|
|
||||||
elif i > fieldnum:
|
|
||||||
values[field.name] = field.get_min(dateval)
|
|
||||||
else:
|
|
||||||
values[field.name] = new_value
|
|
||||||
|
|
||||||
return self.timezone.localize(datetime(**values))
|
|
||||||
|
|
||||||
def get_next_fire_time(self, previous_fire_time, now):
|
|
||||||
if previous_fire_time:
|
|
||||||
start_date = min(now, previous_fire_time + timedelta(microseconds=1))
|
|
||||||
if start_date == previous_fire_time:
|
|
||||||
start_date += timedelta(microseconds=1)
|
|
||||||
else:
|
|
||||||
start_date = max(now, self.start_date) if self.start_date else now
|
|
||||||
|
|
||||||
fieldnum = 0
|
|
||||||
next_date = datetime_ceil(start_date).astimezone(self.timezone)
|
|
||||||
while 0 <= fieldnum < len(self.fields):
|
|
||||||
field = self.fields[fieldnum]
|
|
||||||
curr_value = field.get_value(next_date)
|
|
||||||
next_value = field.get_next_value(next_date)
|
|
||||||
|
|
||||||
if next_value is None:
|
|
||||||
# No valid value was found
|
|
||||||
next_date, fieldnum = self._increment_field_value(next_date, fieldnum - 1)
|
|
||||||
elif next_value > curr_value:
|
|
||||||
# A valid, but higher than the starting value, was found
|
|
||||||
if field.REAL:
|
|
||||||
next_date = self._set_field_value(next_date, fieldnum, next_value)
|
|
||||||
fieldnum += 1
|
|
||||||
else:
|
|
||||||
next_date, fieldnum = self._increment_field_value(next_date, fieldnum)
|
|
||||||
else:
|
|
||||||
# A valid value was found, no changes necessary
|
|
||||||
fieldnum += 1
|
|
||||||
|
|
||||||
# Return if the date has rolled past the end date
|
|
||||||
if self.end_date and next_date > self.end_date:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if fieldnum >= 0:
|
|
||||||
next_date = self._apply_jitter(next_date, self.jitter, now)
|
|
||||||
return min(next_date, self.end_date) if self.end_date else next_date
|
|
||||||
|
|
||||||
def __getstate__(self):
|
|
||||||
return {
|
|
||||||
'version': 2,
|
|
||||||
'timezone': self.timezone,
|
|
||||||
'start_date': self.start_date,
|
|
||||||
'end_date': self.end_date,
|
|
||||||
'fields': self.fields,
|
|
||||||
'jitter': self.jitter,
|
|
||||||
}
|
|
||||||
|
|
||||||
def __setstate__(self, state):
|
|
||||||
# This is for compatibility with APScheduler 3.0.x
|
|
||||||
if isinstance(state, tuple):
|
|
||||||
state = state[1]
|
|
||||||
|
|
||||||
if state.get('version', 1) > 2:
|
|
||||||
raise ValueError(
|
|
||||||
'Got serialized data for version %s of %s, but only versions up to 2 can be '
|
|
||||||
'handled' % (state['version'], self.__class__.__name__))
|
|
||||||
|
|
||||||
self.timezone = state['timezone']
|
|
||||||
self.start_date = state['start_date']
|
|
||||||
self.end_date = state['end_date']
|
|
||||||
self.fields = state['fields']
|
|
||||||
self.jitter = state.get('jitter')
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
options = ["%s='%s'" % (f.name, f) for f in self.fields if not f.is_default]
|
|
||||||
return 'cron[%s]' % (', '.join(options))
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
options = ["%s='%s'" % (f.name, f) for f in self.fields if not f.is_default]
|
|
||||||
if self.start_date:
|
|
||||||
options.append("start_date=%r" % datetime_repr(self.start_date))
|
|
||||||
if self.end_date:
|
|
||||||
options.append("end_date=%r" % datetime_repr(self.end_date))
|
|
||||||
if self.jitter:
|
|
||||||
options.append('jitter=%s' % self.jitter)
|
|
||||||
|
|
||||||
return "<%s (%s, timezone='%s')>" % (
|
|
||||||
self.__class__.__name__, ', '.join(options), self.timezone)
|
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
"""This module contains the expressions applicable for CronTrigger's fields."""
|
|
||||||
|
|
||||||
from calendar import monthrange
|
|
||||||
import re
|
|
||||||
|
|
||||||
from apscheduler.util import asint
|
|
||||||
|
|
||||||
__all__ = ('AllExpression', 'RangeExpression', 'WeekdayRangeExpression',
|
|
||||||
'WeekdayPositionExpression', 'LastDayOfMonthExpression')
|
|
||||||
|
|
||||||
|
|
||||||
WEEKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
|
|
||||||
MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']
|
|
||||||
|
|
||||||
|
|
||||||
class AllExpression(object):
|
|
||||||
value_re = re.compile(r'\*(?:/(?P<step>\d+))?$')
|
|
||||||
|
|
||||||
def __init__(self, step=None):
|
|
||||||
self.step = asint(step)
|
|
||||||
if self.step == 0:
|
|
||||||
raise ValueError('Increment must be higher than 0')
|
|
||||||
|
|
||||||
def validate_range(self, field_name):
|
|
||||||
from apscheduler.triggers.cron.fields import MIN_VALUES, MAX_VALUES
|
|
||||||
|
|
||||||
value_range = MAX_VALUES[field_name] - MIN_VALUES[field_name]
|
|
||||||
if self.step and self.step > value_range:
|
|
||||||
raise ValueError('the step value ({}) is higher than the total range of the '
|
|
||||||
'expression ({})'.format(self.step, value_range))
|
|
||||||
|
|
||||||
def get_next_value(self, date, field):
|
|
||||||
start = field.get_value(date)
|
|
||||||
minval = field.get_min(date)
|
|
||||||
maxval = field.get_max(date)
|
|
||||||
start = max(start, minval)
|
|
||||||
|
|
||||||
if not self.step:
|
|
||||||
next = start
|
|
||||||
else:
|
|
||||||
distance_to_next = (self.step - (start - minval)) % self.step
|
|
||||||
next = start + distance_to_next
|
|
||||||
|
|
||||||
if next <= maxval:
|
|
||||||
return next
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
return isinstance(other, self.__class__) and self.step == other.step
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
if self.step:
|
|
||||||
return '*/%d' % self.step
|
|
||||||
return '*'
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "%s(%s)" % (self.__class__.__name__, self.step)
|
|
||||||
|
|
||||||
|
|
||||||
class RangeExpression(AllExpression):
|
|
||||||
value_re = re.compile(
|
|
||||||
r'(?P<first>\d+)(?:-(?P<last>\d+))?(?:/(?P<step>\d+))?$')
|
|
||||||
|
|
||||||
def __init__(self, first, last=None, step=None):
|
|
||||||
super(RangeExpression, self).__init__(step)
|
|
||||||
first = asint(first)
|
|
||||||
last = asint(last)
|
|
||||||
if last is None and step is None:
|
|
||||||
last = first
|
|
||||||
if last is not None and first > last:
|
|
||||||
raise ValueError('The minimum value in a range must not be higher than the maximum')
|
|
||||||
self.first = first
|
|
||||||
self.last = last
|
|
||||||
|
|
||||||
def validate_range(self, field_name):
|
|
||||||
from apscheduler.triggers.cron.fields import MIN_VALUES, MAX_VALUES
|
|
||||||
|
|
||||||
super(RangeExpression, self).validate_range(field_name)
|
|
||||||
if self.first < MIN_VALUES[field_name]:
|
|
||||||
raise ValueError('the first value ({}) is lower than the minimum value ({})'
|
|
||||||
.format(self.first, MIN_VALUES[field_name]))
|
|
||||||
if self.last is not None and self.last > MAX_VALUES[field_name]:
|
|
||||||
raise ValueError('the last value ({}) is higher than the maximum value ({})'
|
|
||||||
.format(self.last, MAX_VALUES[field_name]))
|
|
||||||
value_range = (self.last or MAX_VALUES[field_name]) - self.first
|
|
||||||
if self.step and self.step > value_range:
|
|
||||||
raise ValueError('the step value ({}) is higher than the total range of the '
|
|
||||||
'expression ({})'.format(self.step, value_range))
|
|
||||||
|
|
||||||
def get_next_value(self, date, field):
|
|
||||||
startval = field.get_value(date)
|
|
||||||
minval = field.get_min(date)
|
|
||||||
maxval = field.get_max(date)
|
|
||||||
|
|
||||||
# Apply range limits
|
|
||||||
minval = max(minval, self.first)
|
|
||||||
maxval = min(maxval, self.last) if self.last is not None else maxval
|
|
||||||
nextval = max(minval, startval)
|
|
||||||
|
|
||||||
# Apply the step if defined
|
|
||||||
if self.step:
|
|
||||||
distance_to_next = (self.step - (nextval - minval)) % self.step
|
|
||||||
nextval += distance_to_next
|
|
||||||
|
|
||||||
return nextval if nextval <= maxval else None
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
return (isinstance(other, self.__class__) and self.first == other.first and
|
|
||||||
self.last == other.last)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
if self.last != self.first and self.last is not None:
|
|
||||||
range = '%d-%d' % (self.first, self.last)
|
|
||||||
else:
|
|
||||||
range = str(self.first)
|
|
||||||
|
|
||||||
if self.step:
|
|
||||||
return '%s/%d' % (range, self.step)
|
|
||||||
return range
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
args = [str(self.first)]
|
|
||||||
if self.last != self.first and self.last is not None or self.step:
|
|
||||||
args.append(str(self.last))
|
|
||||||
if self.step:
|
|
||||||
args.append(str(self.step))
|
|
||||||
return "%s(%s)" % (self.__class__.__name__, ', '.join(args))
|
|
||||||
|
|
||||||
|
|
||||||
class MonthRangeExpression(RangeExpression):
|
|
||||||
value_re = re.compile(r'(?P<first>[a-z]+)(?:-(?P<last>[a-z]+))?', re.IGNORECASE)
|
|
||||||
|
|
||||||
def __init__(self, first, last=None):
|
|
||||||
try:
|
|
||||||
first_num = MONTHS.index(first.lower()) + 1
|
|
||||||
except ValueError:
|
|
||||||
raise ValueError('Invalid month name "%s"' % first)
|
|
||||||
|
|
||||||
if last:
|
|
||||||
try:
|
|
||||||
last_num = MONTHS.index(last.lower()) + 1
|
|
||||||
except ValueError:
|
|
||||||
raise ValueError('Invalid month name "%s"' % last)
|
|
||||||
else:
|
|
||||||
last_num = None
|
|
||||||
|
|
||||||
super(MonthRangeExpression, self).__init__(first_num, last_num)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
if self.last != self.first and self.last is not None:
|
|
||||||
return '%s-%s' % (MONTHS[self.first - 1], MONTHS[self.last - 1])
|
|
||||||
return MONTHS[self.first - 1]
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
args = ["'%s'" % MONTHS[self.first]]
|
|
||||||
if self.last != self.first and self.last is not None:
|
|
||||||
args.append("'%s'" % MONTHS[self.last - 1])
|
|
||||||
return "%s(%s)" % (self.__class__.__name__, ', '.join(args))
|
|
||||||
|
|
||||||
|
|
||||||
class WeekdayRangeExpression(RangeExpression):
|
|
||||||
value_re = re.compile(r'(?P<first>[a-z]+)(?:-(?P<last>[a-z]+))?', re.IGNORECASE)
|
|
||||||
|
|
||||||
def __init__(self, first, last=None):
|
|
||||||
try:
|
|
||||||
first_num = WEEKDAYS.index(first.lower())
|
|
||||||
except ValueError:
|
|
||||||
raise ValueError('Invalid weekday name "%s"' % first)
|
|
||||||
|
|
||||||
if last:
|
|
||||||
try:
|
|
||||||
last_num = WEEKDAYS.index(last.lower())
|
|
||||||
except ValueError:
|
|
||||||
raise ValueError('Invalid weekday name "%s"' % last)
|
|
||||||
else:
|
|
||||||
last_num = None
|
|
||||||
|
|
||||||
super(WeekdayRangeExpression, self).__init__(first_num, last_num)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
if self.last != self.first and self.last is not None:
|
|
||||||
return '%s-%s' % (WEEKDAYS[self.first], WEEKDAYS[self.last])
|
|
||||||
return WEEKDAYS[self.first]
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
args = ["'%s'" % WEEKDAYS[self.first]]
|
|
||||||
if self.last != self.first and self.last is not None:
|
|
||||||
args.append("'%s'" % WEEKDAYS[self.last])
|
|
||||||
return "%s(%s)" % (self.__class__.__name__, ', '.join(args))
|
|
||||||
|
|
||||||
|
|
||||||
class WeekdayPositionExpression(AllExpression):
|
|
||||||
options = ['1st', '2nd', '3rd', '4th', '5th', 'last']
|
|
||||||
value_re = re.compile(r'(?P<option_name>%s) +(?P<weekday_name>(?:\d+|\w+))' %
|
|
||||||
'|'.join(options), re.IGNORECASE)
|
|
||||||
|
|
||||||
def __init__(self, option_name, weekday_name):
|
|
||||||
super(WeekdayPositionExpression, self).__init__(None)
|
|
||||||
try:
|
|
||||||
self.option_num = self.options.index(option_name.lower())
|
|
||||||
except ValueError:
|
|
||||||
raise ValueError('Invalid weekday position "%s"' % option_name)
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.weekday = WEEKDAYS.index(weekday_name.lower())
|
|
||||||
except ValueError:
|
|
||||||
raise ValueError('Invalid weekday name "%s"' % weekday_name)
|
|
||||||
|
|
||||||
def get_next_value(self, date, field):
|
|
||||||
# Figure out the weekday of the month's first day and the number of days in that month
|
|
||||||
first_day_wday, last_day = monthrange(date.year, date.month)
|
|
||||||
|
|
||||||
# Calculate which day of the month is the first of the target weekdays
|
|
||||||
first_hit_day = self.weekday - first_day_wday + 1
|
|
||||||
if first_hit_day <= 0:
|
|
||||||
first_hit_day += 7
|
|
||||||
|
|
||||||
# Calculate what day of the month the target weekday would be
|
|
||||||
if self.option_num < 5:
|
|
||||||
target_day = first_hit_day + self.option_num * 7
|
|
||||||
else:
|
|
||||||
target_day = first_hit_day + ((last_day - first_hit_day) // 7) * 7
|
|
||||||
|
|
||||||
if target_day <= last_day and target_day >= date.day:
|
|
||||||
return target_day
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
return (super(WeekdayPositionExpression, self).__eq__(other) and
|
|
||||||
self.option_num == other.option_num and self.weekday == other.weekday)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return '%s %s' % (self.options[self.option_num], WEEKDAYS[self.weekday])
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "%s('%s', '%s')" % (self.__class__.__name__, self.options[self.option_num],
|
|
||||||
WEEKDAYS[self.weekday])
|
|
||||||
|
|
||||||
|
|
||||||
class LastDayOfMonthExpression(AllExpression):
|
|
||||||
value_re = re.compile(r'last', re.IGNORECASE)
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super(LastDayOfMonthExpression, self).__init__(None)
|
|
||||||
|
|
||||||
def get_next_value(self, date, field):
|
|
||||||
return monthrange(date.year, date.month)[1]
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return 'last'
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "%s()" % self.__class__.__name__
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
"""Fields represent CronTrigger options which map to :class:`~datetime.datetime` fields."""
|
|
||||||
|
|
||||||
from calendar import monthrange
|
|
||||||
import re
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from apscheduler.triggers.cron.expressions import (
|
|
||||||
AllExpression, RangeExpression, WeekdayPositionExpression, LastDayOfMonthExpression,
|
|
||||||
WeekdayRangeExpression, MonthRangeExpression)
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ('MIN_VALUES', 'MAX_VALUES', 'DEFAULT_VALUES', 'BaseField', 'WeekField',
|
|
||||||
'DayOfMonthField', 'DayOfWeekField')
|
|
||||||
|
|
||||||
|
|
||||||
MIN_VALUES = {'year': 1970, 'month': 1, 'day': 1, 'week': 1, 'day_of_week': 0, 'hour': 0,
|
|
||||||
'minute': 0, 'second': 0}
|
|
||||||
MAX_VALUES = {'year': 9999, 'month': 12, 'day': 31, 'week': 53, 'day_of_week': 6, 'hour': 23,
|
|
||||||
'minute': 59, 'second': 59}
|
|
||||||
DEFAULT_VALUES = {'year': '*', 'month': 1, 'day': 1, 'week': '*', 'day_of_week': '*', 'hour': 0,
|
|
||||||
'minute': 0, 'second': 0}
|
|
||||||
SEPARATOR = re.compile(' *, *')
|
|
||||||
|
|
||||||
|
|
||||||
class BaseField(object):
|
|
||||||
REAL = True
|
|
||||||
COMPILERS = [AllExpression, RangeExpression]
|
|
||||||
|
|
||||||
def __init__(self, name, exprs, is_default=False):
|
|
||||||
self.name = name
|
|
||||||
self.is_default = is_default
|
|
||||||
self.compile_expressions(exprs)
|
|
||||||
|
|
||||||
def get_min(self, dateval):
|
|
||||||
return MIN_VALUES[self.name]
|
|
||||||
|
|
||||||
def get_max(self, dateval):
|
|
||||||
return MAX_VALUES[self.name]
|
|
||||||
|
|
||||||
def get_value(self, dateval):
|
|
||||||
return getattr(dateval, self.name)
|
|
||||||
|
|
||||||
def get_next_value(self, dateval):
|
|
||||||
smallest = None
|
|
||||||
for expr in self.expressions:
|
|
||||||
value = expr.get_next_value(dateval, self)
|
|
||||||
if smallest is None or (value is not None and value < smallest):
|
|
||||||
smallest = value
|
|
||||||
|
|
||||||
return smallest
|
|
||||||
|
|
||||||
def compile_expressions(self, exprs):
|
|
||||||
self.expressions = []
|
|
||||||
|
|
||||||
# Split a comma-separated expression list, if any
|
|
||||||
for expr in SEPARATOR.split(str(exprs).strip()):
|
|
||||||
self.compile_expression(expr)
|
|
||||||
|
|
||||||
def compile_expression(self, expr):
|
|
||||||
for compiler in self.COMPILERS:
|
|
||||||
match = compiler.value_re.match(expr)
|
|
||||||
if match:
|
|
||||||
compiled_expr = compiler(**match.groupdict())
|
|
||||||
|
|
||||||
try:
|
|
||||||
compiled_expr.validate_range(self.name)
|
|
||||||
except ValueError as e:
|
|
||||||
exc = ValueError('Error validating expression {!r}: {}'.format(expr, e))
|
|
||||||
six.raise_from(exc, None)
|
|
||||||
|
|
||||||
self.expressions.append(compiled_expr)
|
|
||||||
return
|
|
||||||
|
|
||||||
raise ValueError('Unrecognized expression "%s" for field "%s"' % (expr, self.name))
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
return isinstance(self, self.__class__) and self.expressions == other.expressions
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
expr_strings = (str(e) for e in self.expressions)
|
|
||||||
return ','.join(expr_strings)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "%s('%s', '%s')" % (self.__class__.__name__, self.name, self)
|
|
||||||
|
|
||||||
|
|
||||||
class WeekField(BaseField):
|
|
||||||
REAL = False
|
|
||||||
|
|
||||||
def get_value(self, dateval):
|
|
||||||
return dateval.isocalendar()[1]
|
|
||||||
|
|
||||||
|
|
||||||
class DayOfMonthField(BaseField):
|
|
||||||
COMPILERS = BaseField.COMPILERS + [WeekdayPositionExpression, LastDayOfMonthExpression]
|
|
||||||
|
|
||||||
def get_max(self, dateval):
|
|
||||||
return monthrange(dateval.year, dateval.month)[1]
|
|
||||||
|
|
||||||
|
|
||||||
class DayOfWeekField(BaseField):
|
|
||||||
REAL = False
|
|
||||||
COMPILERS = BaseField.COMPILERS + [WeekdayRangeExpression]
|
|
||||||
|
|
||||||
def get_value(self, dateval):
|
|
||||||
return dateval.weekday()
|
|
||||||
|
|
||||||
|
|
||||||
class MonthField(BaseField):
|
|
||||||
COMPILERS = BaseField.COMPILERS + [MonthRangeExpression]
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from tzlocal import get_localzone
|
|
||||||
|
|
||||||
from apscheduler.triggers.base import BaseTrigger
|
|
||||||
from apscheduler.util import convert_to_datetime, datetime_repr, astimezone
|
|
||||||
|
|
||||||
|
|
||||||
class DateTrigger(BaseTrigger):
|
|
||||||
"""
|
|
||||||
Triggers once on the given datetime. If ``run_date`` is left empty, current time is used.
|
|
||||||
|
|
||||||
:param datetime|str run_date: the date/time to run the job at
|
|
||||||
:param datetime.tzinfo|str timezone: time zone for ``run_date`` if it doesn't have one already
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = 'run_date'
|
|
||||||
|
|
||||||
def __init__(self, run_date=None, timezone=None):
|
|
||||||
timezone = astimezone(timezone) or get_localzone()
|
|
||||||
if run_date is not None:
|
|
||||||
self.run_date = convert_to_datetime(run_date, timezone, 'run_date')
|
|
||||||
else:
|
|
||||||
self.run_date = datetime.now(timezone)
|
|
||||||
|
|
||||||
def get_next_fire_time(self, previous_fire_time, now):
|
|
||||||
return self.run_date if previous_fire_time is None else None
|
|
||||||
|
|
||||||
def __getstate__(self):
|
|
||||||
return {
|
|
||||||
'version': 1,
|
|
||||||
'run_date': self.run_date
|
|
||||||
}
|
|
||||||
|
|
||||||
def __setstate__(self, state):
|
|
||||||
# This is for compatibility with APScheduler 3.0.x
|
|
||||||
if isinstance(state, tuple):
|
|
||||||
state = state[1]
|
|
||||||
|
|
||||||
if state.get('version', 1) > 1:
|
|
||||||
raise ValueError(
|
|
||||||
'Got serialized data for version %s of %s, but only version 1 can be handled' %
|
|
||||||
(state['version'], self.__class__.__name__))
|
|
||||||
|
|
||||||
self.run_date = state['run_date']
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return 'date[%s]' % datetime_repr(self.run_date)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "<%s (run_date='%s')>" % (self.__class__.__name__, datetime_repr(self.run_date))
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
from datetime import timedelta, datetime
|
|
||||||
from math import ceil
|
|
||||||
|
|
||||||
from tzlocal import get_localzone
|
|
||||||
|
|
||||||
from apscheduler.triggers.base import BaseTrigger
|
|
||||||
from apscheduler.util import convert_to_datetime, timedelta_seconds, datetime_repr, astimezone
|
|
||||||
|
|
||||||
|
|
||||||
class IntervalTrigger(BaseTrigger):
|
|
||||||
"""
|
|
||||||
Triggers on specified intervals, starting on ``start_date`` if specified, ``datetime.now()`` +
|
|
||||||
interval otherwise.
|
|
||||||
|
|
||||||
:param int weeks: number of weeks to wait
|
|
||||||
:param int days: number of days to wait
|
|
||||||
:param int hours: number of hours to wait
|
|
||||||
:param int minutes: number of minutes to wait
|
|
||||||
:param int seconds: number of seconds to wait
|
|
||||||
:param datetime|str start_date: starting point for the interval calculation
|
|
||||||
:param datetime|str end_date: latest possible date/time to trigger on
|
|
||||||
:param datetime.tzinfo|str timezone: time zone to use for the date/time calculations
|
|
||||||
:param int|None jitter: advance or delay the job execution by ``jitter`` seconds at most.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = 'timezone', 'start_date', 'end_date', 'interval', 'interval_length', 'jitter'
|
|
||||||
|
|
||||||
def __init__(self, weeks=0, days=0, hours=0, minutes=0, seconds=0, start_date=None,
|
|
||||||
end_date=None, timezone=None, jitter=None):
|
|
||||||
self.interval = timedelta(weeks=weeks, days=days, hours=hours, minutes=minutes,
|
|
||||||
seconds=seconds)
|
|
||||||
self.interval_length = timedelta_seconds(self.interval)
|
|
||||||
if self.interval_length == 0:
|
|
||||||
self.interval = timedelta(seconds=1)
|
|
||||||
self.interval_length = 1
|
|
||||||
|
|
||||||
if timezone:
|
|
||||||
self.timezone = astimezone(timezone)
|
|
||||||
elif isinstance(start_date, datetime) and start_date.tzinfo:
|
|
||||||
self.timezone = start_date.tzinfo
|
|
||||||
elif isinstance(end_date, datetime) and end_date.tzinfo:
|
|
||||||
self.timezone = end_date.tzinfo
|
|
||||||
else:
|
|
||||||
self.timezone = get_localzone()
|
|
||||||
|
|
||||||
start_date = start_date or (datetime.now(self.timezone) + self.interval)
|
|
||||||
self.start_date = convert_to_datetime(start_date, self.timezone, 'start_date')
|
|
||||||
self.end_date = convert_to_datetime(end_date, self.timezone, 'end_date')
|
|
||||||
|
|
||||||
self.jitter = jitter
|
|
||||||
|
|
||||||
def get_next_fire_time(self, previous_fire_time, now):
|
|
||||||
if previous_fire_time:
|
|
||||||
next_fire_time = previous_fire_time + self.interval
|
|
||||||
elif self.start_date > now:
|
|
||||||
next_fire_time = self.start_date
|
|
||||||
else:
|
|
||||||
timediff_seconds = timedelta_seconds(now - self.start_date)
|
|
||||||
next_interval_num = int(ceil(timediff_seconds / self.interval_length))
|
|
||||||
next_fire_time = self.start_date + self.interval * next_interval_num
|
|
||||||
|
|
||||||
if self.jitter is not None:
|
|
||||||
next_fire_time = self._apply_jitter(next_fire_time, self.jitter, now)
|
|
||||||
|
|
||||||
if not self.end_date or next_fire_time <= self.end_date:
|
|
||||||
return self.timezone.normalize(next_fire_time)
|
|
||||||
|
|
||||||
def __getstate__(self):
|
|
||||||
return {
|
|
||||||
'version': 2,
|
|
||||||
'timezone': self.timezone,
|
|
||||||
'start_date': self.start_date,
|
|
||||||
'end_date': self.end_date,
|
|
||||||
'interval': self.interval,
|
|
||||||
'jitter': self.jitter,
|
|
||||||
}
|
|
||||||
|
|
||||||
def __setstate__(self, state):
|
|
||||||
# This is for compatibility with APScheduler 3.0.x
|
|
||||||
if isinstance(state, tuple):
|
|
||||||
state = state[1]
|
|
||||||
|
|
||||||
if state.get('version', 1) > 2:
|
|
||||||
raise ValueError(
|
|
||||||
'Got serialized data for version %s of %s, but only versions up to 2 can be '
|
|
||||||
'handled' % (state['version'], self.__class__.__name__))
|
|
||||||
|
|
||||||
self.timezone = state['timezone']
|
|
||||||
self.start_date = state['start_date']
|
|
||||||
self.end_date = state['end_date']
|
|
||||||
self.interval = state['interval']
|
|
||||||
self.interval_length = timedelta_seconds(self.interval)
|
|
||||||
self.jitter = state.get('jitter')
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return 'interval[%s]' % str(self.interval)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
options = ['interval=%r' % self.interval, 'start_date=%r' % datetime_repr(self.start_date)]
|
|
||||||
if self.end_date:
|
|
||||||
options.append("end_date=%r" % datetime_repr(self.end_date))
|
|
||||||
if self.jitter:
|
|
||||||
options.append('jitter=%s' % self.jitter)
|
|
||||||
|
|
||||||
return "<%s (%s, timezone='%s')>" % (
|
|
||||||
self.__class__.__name__, ', '.join(options), self.timezone)
|
|
||||||
@@ -1,429 +0,0 @@
|
|||||||
"""This module contains several handy functions primarily meant for internal use."""
|
|
||||||
|
|
||||||
from __future__ import division
|
|
||||||
|
|
||||||
from datetime import date, datetime, time, timedelta, tzinfo
|
|
||||||
from calendar import timegm
|
|
||||||
from functools import partial
|
|
||||||
from inspect import isclass, ismethod
|
|
||||||
import re
|
|
||||||
|
|
||||||
from pytz import timezone, utc, FixedOffset
|
|
||||||
import six
|
|
||||||
|
|
||||||
try:
|
|
||||||
from inspect import signature
|
|
||||||
except ImportError: # pragma: nocover
|
|
||||||
from funcsigs import signature
|
|
||||||
|
|
||||||
try:
|
|
||||||
from threading import TIMEOUT_MAX
|
|
||||||
except ImportError:
|
|
||||||
TIMEOUT_MAX = 4294967 # Maximum value accepted by Event.wait() on Windows
|
|
||||||
|
|
||||||
try:
|
|
||||||
from asyncio import iscoroutinefunction
|
|
||||||
except ImportError:
|
|
||||||
try:
|
|
||||||
from trollius import iscoroutinefunction
|
|
||||||
except ImportError:
|
|
||||||
def iscoroutinefunction(func):
|
|
||||||
return False
|
|
||||||
|
|
||||||
__all__ = ('asint', 'asbool', 'astimezone', 'convert_to_datetime', 'datetime_to_utc_timestamp',
|
|
||||||
'utc_timestamp_to_datetime', 'timedelta_seconds', 'datetime_ceil', 'get_callable_name',
|
|
||||||
'obj_to_ref', 'ref_to_obj', 'maybe_ref', 'repr_escape', 'check_callable_args',
|
|
||||||
'TIMEOUT_MAX')
|
|
||||||
|
|
||||||
|
|
||||||
class _Undefined(object):
|
|
||||||
def __nonzero__(self):
|
|
||||||
return False
|
|
||||||
|
|
||||||
def __bool__(self):
|
|
||||||
return False
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<undefined>'
|
|
||||||
|
|
||||||
|
|
||||||
undefined = _Undefined() #: a unique object that only signifies that no value is defined
|
|
||||||
|
|
||||||
|
|
||||||
def asint(text):
|
|
||||||
"""
|
|
||||||
Safely converts a string to an integer, returning ``None`` if the string is ``None``.
|
|
||||||
|
|
||||||
:type text: str
|
|
||||||
:rtype: int
|
|
||||||
|
|
||||||
"""
|
|
||||||
if text is not None:
|
|
||||||
return int(text)
|
|
||||||
|
|
||||||
|
|
||||||
def asbool(obj):
|
|
||||||
"""
|
|
||||||
Interprets an object as a boolean value.
|
|
||||||
|
|
||||||
:rtype: bool
|
|
||||||
|
|
||||||
"""
|
|
||||||
if isinstance(obj, str):
|
|
||||||
obj = obj.strip().lower()
|
|
||||||
if obj in ('true', 'yes', 'on', 'y', 't', '1'):
|
|
||||||
return True
|
|
||||||
if obj in ('false', 'no', 'off', 'n', 'f', '0'):
|
|
||||||
return False
|
|
||||||
raise ValueError('Unable to interpret value "%s" as boolean' % obj)
|
|
||||||
return bool(obj)
|
|
||||||
|
|
||||||
|
|
||||||
def astimezone(obj):
|
|
||||||
"""
|
|
||||||
Interprets an object as a timezone.
|
|
||||||
|
|
||||||
:rtype: tzinfo
|
|
||||||
|
|
||||||
"""
|
|
||||||
if isinstance(obj, six.string_types):
|
|
||||||
return timezone(obj)
|
|
||||||
if isinstance(obj, tzinfo):
|
|
||||||
if not hasattr(obj, 'localize') or not hasattr(obj, 'normalize'):
|
|
||||||
raise TypeError('Only timezones from the pytz library are supported')
|
|
||||||
if obj.zone == 'local':
|
|
||||||
raise ValueError(
|
|
||||||
'Unable to determine the name of the local timezone -- you must explicitly '
|
|
||||||
'specify the name of the local timezone. Please refrain from using timezones like '
|
|
||||||
'EST to prevent problems with daylight saving time. Instead, use a locale based '
|
|
||||||
'timezone name (such as Europe/Helsinki).')
|
|
||||||
return obj
|
|
||||||
if obj is not None:
|
|
||||||
raise TypeError('Expected tzinfo, got %s instead' % obj.__class__.__name__)
|
|
||||||
|
|
||||||
|
|
||||||
_DATE_REGEX = re.compile(
|
|
||||||
r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})'
|
|
||||||
r'(?:[ T](?P<hour>\d{1,2}):(?P<minute>\d{1,2}):(?P<second>\d{1,2})'
|
|
||||||
r'(?:\.(?P<microsecond>\d{1,6}))?'
|
|
||||||
r'(?P<timezone>Z|[+-]\d\d:\d\d)?)?$')
|
|
||||||
|
|
||||||
|
|
||||||
def convert_to_datetime(input, tz, arg_name):
|
|
||||||
"""
|
|
||||||
Converts the given object to a timezone aware datetime object.
|
|
||||||
|
|
||||||
If a timezone aware datetime object is passed, it is returned unmodified.
|
|
||||||
If a native datetime object is passed, it is given the specified timezone.
|
|
||||||
If the input is a string, it is parsed as a datetime with the given timezone.
|
|
||||||
|
|
||||||
Date strings are accepted in three different forms: date only (Y-m-d), date with time
|
|
||||||
(Y-m-d H:M:S) or with date+time with microseconds (Y-m-d H:M:S.micro). Additionally you can
|
|
||||||
override the time zone by giving a specific offset in the format specified by ISO 8601:
|
|
||||||
Z (UTC), +HH:MM or -HH:MM.
|
|
||||||
|
|
||||||
:param str|datetime input: the datetime or string to convert to a timezone aware datetime
|
|
||||||
:param datetime.tzinfo tz: timezone to interpret ``input`` in
|
|
||||||
:param str arg_name: the name of the argument (used in an error message)
|
|
||||||
:rtype: datetime
|
|
||||||
|
|
||||||
"""
|
|
||||||
if input is None:
|
|
||||||
return
|
|
||||||
elif isinstance(input, datetime):
|
|
||||||
datetime_ = input
|
|
||||||
elif isinstance(input, date):
|
|
||||||
datetime_ = datetime.combine(input, time())
|
|
||||||
elif isinstance(input, six.string_types):
|
|
||||||
m = _DATE_REGEX.match(input)
|
|
||||||
if not m:
|
|
||||||
raise ValueError('Invalid date string')
|
|
||||||
|
|
||||||
values = m.groupdict()
|
|
||||||
tzname = values.pop('timezone')
|
|
||||||
if tzname == 'Z':
|
|
||||||
tz = utc
|
|
||||||
elif tzname:
|
|
||||||
hours, minutes = (int(x) for x in tzname[1:].split(':'))
|
|
||||||
sign = 1 if tzname[0] == '+' else -1
|
|
||||||
tz = FixedOffset(sign * (hours * 60 + minutes))
|
|
||||||
|
|
||||||
values = {k: int(v or 0) for k, v in values.items()}
|
|
||||||
datetime_ = datetime(**values)
|
|
||||||
else:
|
|
||||||
raise TypeError('Unsupported type for %s: %s' % (arg_name, input.__class__.__name__))
|
|
||||||
|
|
||||||
if datetime_.tzinfo is not None:
|
|
||||||
return datetime_
|
|
||||||
if tz is None:
|
|
||||||
raise ValueError(
|
|
||||||
'The "tz" argument must be specified if %s has no timezone information' % arg_name)
|
|
||||||
if isinstance(tz, six.string_types):
|
|
||||||
tz = timezone(tz)
|
|
||||||
|
|
||||||
try:
|
|
||||||
return tz.localize(datetime_, is_dst=None)
|
|
||||||
except AttributeError:
|
|
||||||
raise TypeError(
|
|
||||||
'Only pytz timezones are supported (need the localize() and normalize() methods)')
|
|
||||||
|
|
||||||
|
|
||||||
def datetime_to_utc_timestamp(timeval):
|
|
||||||
"""
|
|
||||||
Converts a datetime instance to a timestamp.
|
|
||||||
|
|
||||||
:type timeval: datetime
|
|
||||||
:rtype: float
|
|
||||||
|
|
||||||
"""
|
|
||||||
if timeval is not None:
|
|
||||||
return timegm(timeval.utctimetuple()) + timeval.microsecond / 1000000
|
|
||||||
|
|
||||||
|
|
||||||
def utc_timestamp_to_datetime(timestamp):
|
|
||||||
"""
|
|
||||||
Converts the given timestamp to a datetime instance.
|
|
||||||
|
|
||||||
:type timestamp: float
|
|
||||||
:rtype: datetime
|
|
||||||
|
|
||||||
"""
|
|
||||||
if timestamp is not None:
|
|
||||||
return datetime.fromtimestamp(timestamp, utc)
|
|
||||||
|
|
||||||
|
|
||||||
def timedelta_seconds(delta):
|
|
||||||
"""
|
|
||||||
Converts the given timedelta to seconds.
|
|
||||||
|
|
||||||
:type delta: timedelta
|
|
||||||
:rtype: float
|
|
||||||
|
|
||||||
"""
|
|
||||||
return delta.days * 24 * 60 * 60 + delta.seconds + \
|
|
||||||
delta.microseconds / 1000000.0
|
|
||||||
|
|
||||||
|
|
||||||
def datetime_ceil(dateval):
|
|
||||||
"""
|
|
||||||
Rounds the given datetime object upwards.
|
|
||||||
|
|
||||||
:type dateval: datetime
|
|
||||||
|
|
||||||
"""
|
|
||||||
if dateval.microsecond > 0:
|
|
||||||
return dateval + timedelta(seconds=1, microseconds=-dateval.microsecond)
|
|
||||||
return dateval
|
|
||||||
|
|
||||||
|
|
||||||
def datetime_repr(dateval):
|
|
||||||
return dateval.strftime('%Y-%m-%d %H:%M:%S %Z') if dateval else 'None'
|
|
||||||
|
|
||||||
|
|
||||||
def get_callable_name(func):
|
|
||||||
"""
|
|
||||||
Returns the best available display name for the given function/callable.
|
|
||||||
|
|
||||||
:rtype: str
|
|
||||||
|
|
||||||
"""
|
|
||||||
# the easy case (on Python 3.3+)
|
|
||||||
if hasattr(func, '__qualname__'):
|
|
||||||
return func.__qualname__
|
|
||||||
|
|
||||||
# class methods, bound and unbound methods
|
|
||||||
f_self = getattr(func, '__self__', None) or getattr(func, 'im_self', None)
|
|
||||||
if f_self and hasattr(func, '__name__'):
|
|
||||||
f_class = f_self if isclass(f_self) else f_self.__class__
|
|
||||||
else:
|
|
||||||
f_class = getattr(func, 'im_class', None)
|
|
||||||
|
|
||||||
if f_class and hasattr(func, '__name__'):
|
|
||||||
return '%s.%s' % (f_class.__name__, func.__name__)
|
|
||||||
|
|
||||||
# class or class instance
|
|
||||||
if hasattr(func, '__call__'):
|
|
||||||
# class
|
|
||||||
if hasattr(func, '__name__'):
|
|
||||||
return func.__name__
|
|
||||||
|
|
||||||
# instance of a class with a __call__ method
|
|
||||||
return func.__class__.__name__
|
|
||||||
|
|
||||||
raise TypeError('Unable to determine a name for %r -- maybe it is not a callable?' % func)
|
|
||||||
|
|
||||||
|
|
||||||
def obj_to_ref(obj):
|
|
||||||
"""
|
|
||||||
Returns the path to the given callable.
|
|
||||||
|
|
||||||
:rtype: str
|
|
||||||
:raises TypeError: if the given object is not callable
|
|
||||||
:raises ValueError: if the given object is a :class:`~functools.partial`, lambda or a nested
|
|
||||||
function
|
|
||||||
|
|
||||||
"""
|
|
||||||
if isinstance(obj, partial):
|
|
||||||
raise ValueError('Cannot create a reference to a partial()')
|
|
||||||
|
|
||||||
name = get_callable_name(obj)
|
|
||||||
if '<lambda>' in name:
|
|
||||||
raise ValueError('Cannot create a reference to a lambda')
|
|
||||||
if '<locals>' in name:
|
|
||||||
raise ValueError('Cannot create a reference to a nested function')
|
|
||||||
|
|
||||||
if ismethod(obj):
|
|
||||||
if hasattr(obj, 'im_self') and obj.im_self:
|
|
||||||
# bound method
|
|
||||||
module = obj.im_self.__module__
|
|
||||||
elif hasattr(obj, 'im_class') and obj.im_class:
|
|
||||||
# unbound method
|
|
||||||
module = obj.im_class.__module__
|
|
||||||
else:
|
|
||||||
module = obj.__module__
|
|
||||||
else:
|
|
||||||
module = obj.__module__
|
|
||||||
return '%s:%s' % (module, name)
|
|
||||||
|
|
||||||
|
|
||||||
def ref_to_obj(ref):
|
|
||||||
"""
|
|
||||||
Returns the object pointed to by ``ref``.
|
|
||||||
|
|
||||||
:type ref: str
|
|
||||||
|
|
||||||
"""
|
|
||||||
if not isinstance(ref, six.string_types):
|
|
||||||
raise TypeError('References must be strings')
|
|
||||||
if ':' not in ref:
|
|
||||||
raise ValueError('Invalid reference')
|
|
||||||
|
|
||||||
modulename, rest = ref.split(':', 1)
|
|
||||||
try:
|
|
||||||
obj = __import__(modulename, fromlist=[rest])
|
|
||||||
except ImportError:
|
|
||||||
raise LookupError('Error resolving reference %s: could not import module' % ref)
|
|
||||||
|
|
||||||
try:
|
|
||||||
for name in rest.split('.'):
|
|
||||||
obj = getattr(obj, name)
|
|
||||||
return obj
|
|
||||||
except Exception:
|
|
||||||
raise LookupError('Error resolving reference %s: error looking up object' % ref)
|
|
||||||
|
|
||||||
|
|
||||||
def maybe_ref(ref):
|
|
||||||
"""
|
|
||||||
Returns the object that the given reference points to, if it is indeed a reference.
|
|
||||||
If it is not a reference, the object is returned as-is.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if not isinstance(ref, str):
|
|
||||||
return ref
|
|
||||||
return ref_to_obj(ref)
|
|
||||||
|
|
||||||
|
|
||||||
if six.PY2:
|
|
||||||
def repr_escape(string):
|
|
||||||
if isinstance(string, six.text_type):
|
|
||||||
return string.encode('ascii', 'backslashreplace')
|
|
||||||
return string
|
|
||||||
else:
|
|
||||||
def repr_escape(string):
|
|
||||||
return string
|
|
||||||
|
|
||||||
|
|
||||||
def check_callable_args(func, args, kwargs):
|
|
||||||
"""
|
|
||||||
Ensures that the given callable can be called with the given arguments.
|
|
||||||
|
|
||||||
:type args: tuple
|
|
||||||
:type kwargs: dict
|
|
||||||
|
|
||||||
"""
|
|
||||||
pos_kwargs_conflicts = [] # parameters that have a match in both args and kwargs
|
|
||||||
positional_only_kwargs = [] # positional-only parameters that have a match in kwargs
|
|
||||||
unsatisfied_args = [] # parameters in signature that don't have a match in args or kwargs
|
|
||||||
unsatisfied_kwargs = [] # keyword-only arguments that don't have a match in kwargs
|
|
||||||
unmatched_args = list(args) # args that didn't match any of the parameters in the signature
|
|
||||||
# kwargs that didn't match any of the parameters in the signature
|
|
||||||
unmatched_kwargs = list(kwargs)
|
|
||||||
# indicates if the signature defines *args and **kwargs respectively
|
|
||||||
has_varargs = has_var_kwargs = False
|
|
||||||
|
|
||||||
try:
|
|
||||||
sig = signature(func)
|
|
||||||
except ValueError:
|
|
||||||
# signature() doesn't work against every kind of callable
|
|
||||||
return
|
|
||||||
|
|
||||||
for param in six.itervalues(sig.parameters):
|
|
||||||
if param.kind == param.POSITIONAL_OR_KEYWORD:
|
|
||||||
if param.name in unmatched_kwargs and unmatched_args:
|
|
||||||
pos_kwargs_conflicts.append(param.name)
|
|
||||||
elif unmatched_args:
|
|
||||||
del unmatched_args[0]
|
|
||||||
elif param.name in unmatched_kwargs:
|
|
||||||
unmatched_kwargs.remove(param.name)
|
|
||||||
elif param.default is param.empty:
|
|
||||||
unsatisfied_args.append(param.name)
|
|
||||||
elif param.kind == param.POSITIONAL_ONLY:
|
|
||||||
if unmatched_args:
|
|
||||||
del unmatched_args[0]
|
|
||||||
elif param.name in unmatched_kwargs:
|
|
||||||
unmatched_kwargs.remove(param.name)
|
|
||||||
positional_only_kwargs.append(param.name)
|
|
||||||
elif param.default is param.empty:
|
|
||||||
unsatisfied_args.append(param.name)
|
|
||||||
elif param.kind == param.KEYWORD_ONLY:
|
|
||||||
if param.name in unmatched_kwargs:
|
|
||||||
unmatched_kwargs.remove(param.name)
|
|
||||||
elif param.default is param.empty:
|
|
||||||
unsatisfied_kwargs.append(param.name)
|
|
||||||
elif param.kind == param.VAR_POSITIONAL:
|
|
||||||
has_varargs = True
|
|
||||||
elif param.kind == param.VAR_KEYWORD:
|
|
||||||
has_var_kwargs = True
|
|
||||||
|
|
||||||
# Make sure there are no conflicts between args and kwargs
|
|
||||||
if pos_kwargs_conflicts:
|
|
||||||
raise ValueError('The following arguments are supplied in both args and kwargs: %s' %
|
|
||||||
', '.join(pos_kwargs_conflicts))
|
|
||||||
|
|
||||||
# Check if keyword arguments are being fed to positional-only parameters
|
|
||||||
if positional_only_kwargs:
|
|
||||||
raise ValueError('The following arguments cannot be given as keyword arguments: %s' %
|
|
||||||
', '.join(positional_only_kwargs))
|
|
||||||
|
|
||||||
# Check that the number of positional arguments minus the number of matched kwargs matches the
|
|
||||||
# argspec
|
|
||||||
if unsatisfied_args:
|
|
||||||
raise ValueError('The following arguments have not been supplied: %s' %
|
|
||||||
', '.join(unsatisfied_args))
|
|
||||||
|
|
||||||
# Check that all keyword-only arguments have been supplied
|
|
||||||
if unsatisfied_kwargs:
|
|
||||||
raise ValueError(
|
|
||||||
'The following keyword-only arguments have not been supplied in kwargs: %s' %
|
|
||||||
', '.join(unsatisfied_kwargs))
|
|
||||||
|
|
||||||
# Check that the callable can accept the given number of positional arguments
|
|
||||||
if not has_varargs and unmatched_args:
|
|
||||||
raise ValueError(
|
|
||||||
'The list of positional arguments is longer than the target callable can handle '
|
|
||||||
'(allowed: %d, given in args: %d)' % (len(args) - len(unmatched_args), len(args)))
|
|
||||||
|
|
||||||
# Check that the callable can accept the given keyword arguments
|
|
||||||
if not has_var_kwargs and unmatched_kwargs:
|
|
||||||
raise ValueError(
|
|
||||||
'The target callable does not accept the following keyword arguments: %s' %
|
|
||||||
', '.join(unmatched_kwargs))
|
|
||||||
|
|
||||||
|
|
||||||
def iscoroutinefunction_partial(f):
|
|
||||||
while isinstance(f, partial):
|
|
||||||
f = f.func
|
|
||||||
|
|
||||||
# The asyncio version of iscoroutinefunction includes testing for @coroutine
|
|
||||||
# decorations vs. the inspect version which does not.
|
|
||||||
return iscoroutinefunction(f)
|
|
||||||
2392
lib/argparse.py
2392
lib/argparse.py
File diff suppressed because it is too large
Load Diff
@@ -1,8 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from .arrow import Arrow
|
|
||||||
from .factory import ArrowFactory
|
|
||||||
from .api import get, now, utcnow
|
|
||||||
|
|
||||||
__version__ = '0.10.0'
|
|
||||||
VERSION = __version__
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
'''
|
|
||||||
Provides the default implementation of :class:`ArrowFactory <arrow.factory.ArrowFactory>`
|
|
||||||
methods for use as a module API.
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
from arrow.factory import ArrowFactory
|
|
||||||
|
|
||||||
|
|
||||||
# internal default factory.
|
|
||||||
_factory = ArrowFactory()
|
|
||||||
|
|
||||||
|
|
||||||
def get(*args, **kwargs):
|
|
||||||
''' Implements the default :class:`ArrowFactory <arrow.factory.ArrowFactory>`
|
|
||||||
``get`` method.
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
||||||
return _factory.get(*args, **kwargs)
|
|
||||||
|
|
||||||
def utcnow():
|
|
||||||
''' Implements the default :class:`ArrowFactory <arrow.factory.ArrowFactory>`
|
|
||||||
``utcnow`` method.
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
||||||
return _factory.utcnow()
|
|
||||||
|
|
||||||
|
|
||||||
def now(tz=None):
|
|
||||||
''' Implements the default :class:`ArrowFactory <arrow.factory.ArrowFactory>`
|
|
||||||
``now`` method.
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
||||||
return _factory.now(tz)
|
|
||||||
|
|
||||||
|
|
||||||
def factory(type):
|
|
||||||
''' Returns an :class:`.ArrowFactory` for the specified :class:`Arrow <arrow.arrow.Arrow>`
|
|
||||||
or derived type.
|
|
||||||
|
|
||||||
:param type: the type, :class:`Arrow <arrow.arrow.Arrow>` or derived.
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
||||||
return ArrowFactory(type)
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['get', 'utcnow', 'now', 'factory']
|
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user