diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index e914485d..e9c8e05d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,15 +24,15 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: config-file: ./.github/codeql-config.yml languages: ${{ matrix.language }} - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/issues-stale.yml b/.github/workflows/issues-stale.yml index 26b8aa5f..b805e266 100644 --- a/.github/workflows/issues-stale.yml +++ b/.github/workflows/issues-stale.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Stale - uses: actions/stale@v8 + uses: actions/stale@v9 with: stale-issue-message: > This issue is stale because it has been open for 30 days with no activity. @@ -30,7 +30,7 @@ jobs: days-before-close: 5 - name: Invalid Template - uses: actions/stale@v8 + uses: actions/stale@v9 with: stale-issue-message: > Invalid issues template. diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml index 34ceb357..a60987f5 100644 --- a/.github/workflows/issues.yml +++ b/.github/workflows/issues.yml @@ -10,6 +10,6 @@ jobs: runs-on: ubuntu-latest steps: - name: Label Issues - uses: dessant/label-actions@v3 + uses: dessant/label-actions@v4 with: github-token: ${{ github.token }} diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml index 6d91bbf6..62c3f86c 100644 --- a/.github/workflows/publish-docker.yml +++ b/.github/workflows/publish-docker.yml @@ -13,7 +13,7 @@ jobs: if: ${{ !contains(github.event.head_commit.message, '[skip ci]') }} steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Prepare id: prepare @@ -33,21 +33,20 @@ jobs: echo "branch=${GITHUB_REF#refs/heads/}" >> $GITHUB_OUTPUT fi echo "commit=${GITHUB_SHA}" >> $GITHUB_OUTPUT - echo "build_date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT echo "docker_platforms=linux/amd64,linux/arm64/v8,linux/arm/v7,linux/arm/v6" >> $GITHUB_OUTPUT echo "docker_image=${{ secrets.DOCKER_REPO }}/tautulli" >> $GITHUB_OUTPUT - name: Set Up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 id: buildx with: version: latest - name: Cache Docker Layers - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: /tmp/.buildx-cache key: ${{ runner.os }}-buildx-${{ github.sha }} @@ -55,22 +54,28 @@ jobs: ${{ runner.os }}-buildx- - name: Login to DockerHub - uses: docker/login-action@v2 + uses: docker/login-action@v3 if: success() with: username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} + password: ${{ secrets.DOCKER_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v2 + uses: docker/login-action@v3 if: success() with: registry: ghcr.io username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.GHCR_TOKEN }} + - name: Extract Docker Metadata + id: metadata + uses: docker/metadata-action@v5 + with: + images: ${{ steps.prepare.outputs.docker_image }} + - name: Docker Build and Push - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v6 if: success() with: context: . @@ -81,10 +86,10 @@ jobs: 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 }} + labels: ${{ steps.metadata.outputs.labels }} cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache @@ -94,23 +99,10 @@ jobs: if: always() && !contains(github.event.head_commit.message, '[skip ci]') runs-on: ubuntu-latest steps: - - name: Get Build Job Status - uses: technote-space/workflow-conclusion-action@v3 - - - name: Combine Job Status - id: status - run: | - failures=(neutral, skipped, timed_out, action_required) - if [[ ${array[@]} =~ $WORKFLOW_CONCLUSION ]]; then - echo "status=failure" >> $GITHUB_OUTPUT - else - echo "status=$WORKFLOW_CONCLUSION" >> $GITHUB_OUTPUT - fi - - name: Post Status to Discord uses: sarisia/actions-status-discord@v1 with: webhook: ${{ secrets.DISCORD_WEBHOOK }} - status: ${{ steps.status.outputs.status }} + status: ${{ needs.build-docker.result == 'success' && 'success' || contains(needs.*.result, 'failure') && 'failure' || 'cancelled' }} title: ${{ github.workflow }} nofail: true diff --git a/.github/workflows/publish-installers.yml b/.github/workflows/publish-installers.yml index 0b6eec36..b4a66960 100644 --- a/.github/workflows/publish-installers.yml +++ b/.github/workflows/publish-installers.yml @@ -6,10 +6,13 @@ on: branches: [master, beta, nightly] tags: [v*] +env: + PYTHON_VERSION: '3.11' + jobs: build-installer: name: Build ${{ matrix.os_upper }} Installer - runs-on: ${{ matrix.os }}-latest + runs-on: ${{ matrix.os }}-${{ matrix.os_version }} if: ${{ !contains(github.event.head_commit.message, '[skip ci]') }} strategy: fail-fast: false @@ -17,14 +20,18 @@ jobs: include: - os: 'windows' os_upper: 'Windows' + os_version: 'latest' + arch: 'x64' ext: 'exe' - os: 'macos' os_upper: 'MacOS' + os_version: '14' + arch: 'universal' ext: 'pkg' steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set Release Version id: get_version @@ -52,29 +59,29 @@ jobs: echo $GITHUB_SHA > version.txt - name: Set Up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: '3.9' + python-version: ${{ env.PYTHON_VERSION }} cache: pip cache-dependency-path: '**/requirements*.txt' - name: Install Dependencies run: | python -m pip install --upgrade pip - pip install -r package/requirements-package.txt + pip install -r package/requirements-package.txt --no-binary cffi - name: Build Package run: | pyinstaller -y ./package/Tautulli-${{ matrix.os }}.spec - name: Create Windows Installer - uses: joncloud/makensis-action@v4 + uses: joncloud/makensis-action@v4.1 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 + /DINSTALLER_NAME=..\Tautulli-${{ matrix.os }}-${{ steps.get_version.outputs.RELEASE_VERSION }}-${{ matrix.arch }}.${{ matrix.ext }} additional-plugin-paths: package/nsis-plugins - name: Create MacOS Installer @@ -85,13 +92,31 @@ jobs: --version ${{ steps.get_version.outputs.VERSION }} \ --component ./dist/Tautulli.app \ --scripts ./package/macos-scripts \ - Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.pkg + Tautulli-${{ matrix.os }}-${{ steps.get_version.outputs.RELEASE_VERSION }}-${{ matrix.arch }}.${{ matrix.ext }} - name: Upload Installer - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Tautulli-${{ matrix.os }}-installer - path: Tautulli-${{ matrix.os }}-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.${{ matrix.ext }} + path: Tautulli-${{ matrix.os }}-${{ steps.get_version.outputs.RELEASE_VERSION }}-${{ matrix.arch }}.${{ matrix.ext }} + + virus-total: + name: VirusTotal Scan + needs: build-installer + if: needs.build-installer.result == 'success' && !contains(github.event.head_commit.message, '[skip ci]') + runs-on: ubuntu-latest + steps: + - name: Download Installers + if: needs.build-installer.result == 'success' + uses: actions/download-artifact@v4 + + - name: Upload to VirusTotal + uses: crazy-max/ghaction-virustotal@v4 + with: + vt_api_key: ${{ secrets.VT_API_KEY }} + files: | + Tautulli-windows-installer/Tautulli-windows-*-x64.exe + Tautulli-macos-installer/Tautulli-macos-*-universal.pkg release: name: Release Installers @@ -99,11 +124,8 @@ jobs: if: always() && startsWith(github.ref, 'refs/tags/') && !contains(github.event.head_commit.message, '[skip ci]') runs-on: ubuntu-latest steps: - - name: Get Build Job Status - uses: technote-space/workflow-conclusion-action@v3 - - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set Release Version id: get_version @@ -111,8 +133,8 @@ jobs: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT - name: Download Installers - if: env.WORKFLOW_CONCLUSION == 'success' - uses: actions/download-artifact@v3 + if: needs.build-installer.result == 'success' + uses: actions/download-artifact@v4 - name: Get Changelog id: get_changelog @@ -125,41 +147,21 @@ jobs: echo "$EOF" >> $GITHUB_OUTPUT - name: Create Release - uses: actions/create-release@v1 + uses: softprops/action-gh-release@v2 id: create_release env: GITHUB_TOKEN: ${{ secrets.GHACTIONS_TOKEN }} with: tag_name: ${{ steps.get_version.outputs.RELEASE_VERSION }} - release_name: Tautulli ${{ steps.get_version.outputs.RELEASE_VERSION }} + 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.GHACTIONS_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.GHACTIONS_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 + files: | + Tautulli-windows-installer/Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.exe + Tautulli-macos-installer/Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-universal.pkg discord: name: Discord Notification @@ -167,23 +169,10 @@ jobs: if: always() && !contains(github.event.head_commit.message, '[skip ci]') runs-on: ubuntu-latest steps: - - name: Get Build Job Status - uses: technote-space/workflow-conclusion-action@v3 - - - name: Combine Job Status - id: status - run: | - failures=(neutral, skipped, timed_out, action_required) - if [[ ${array[@]} =~ $WORKFLOW_CONCLUSION ]]; then - echo "status=failure" >> $GITHUB_OUTPUT - else - echo "status=$WORKFLOW_CONCLUSION" >> $GITHUB_OUTPUT - fi - - name: Post Status to Discord uses: sarisia/actions-status-discord@v1 with: webhook: ${{ secrets.DISCORD_WEBHOOK }} - status: ${{ steps.status.outputs.status }} + status: ${{ needs.build-installer.result == 'success' && 'success' || contains(needs.*.result, 'failure') && 'failure' || 'cancelled' }} title: ${{ github.workflow }} nofail: true diff --git a/.github/workflows/publish-snap.yml b/.github/workflows/publish-snap.yml index dd74c3a3..b3898a38 100644 --- a/.github/workflows/publish-snap.yml +++ b/.github/workflows/publish-snap.yml @@ -20,7 +20,7 @@ jobs: - armhf steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Prepare id: prepare @@ -35,22 +35,22 @@ jobs: fi - name: Set Up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Build Snap Package - uses: diddlesnaps/snapcraft-multiarch-action@v1 + uses: diddlesnaps/snapcraft-multiarch-action@master id: build with: architecture: ${{ matrix.architecture }} - name: Upload Snap Package - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Tautulli-snap-package-${{ matrix.architecture }} path: ${{ steps.build.outputs.snap }} - name: Review Snap Package - uses: diddlesnaps/snapcraft-review-tools-action@v1 + uses: diddlesnaps/snapcraft-review-tools-action@master with: snap: ${{ steps.build.outputs.snap }} @@ -69,23 +69,10 @@ jobs: if: always() && !contains(github.event.head_commit.message, '[skip ci]') runs-on: ubuntu-latest steps: - - name: Get Build Job Status - uses: technote-space/workflow-conclusion-action@v3 - - - name: Combine Job Status - id: status - run: | - failures=(neutral, skipped, timed_out, action_required) - if [[ ${array[@]} =~ $WORKFLOW_CONCLUSION ]]; then - echo "status=failure" >> $GITHUB_OUTPUT - else - echo "status=$WORKFLOW_CONCLUSION" >> $GITHUB_OUTPUT - fi - - name: Post Status to Discord uses: sarisia/actions-status-discord@v1 with: webhook: ${{ secrets.DISCORD_WEBHOOK }} - status: ${{ steps.status.outputs.status }} + status: ${{ needs.build-snap.result == 'success' && 'success' || contains(needs.*.result, 'failure') && 'failure' || 'cancelled' }} title: ${{ github.workflow }} nofail: true diff --git a/.github/workflows/pull-requests.yml b/.github/workflows/pull-requests.yml index 1a24cf24..ac550fe2 100644 --- a/.github/workflows/pull-requests.yml +++ b/.github/workflows/pull-requests.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Comment on Pull Request uses: mshick/add-pr-comment@v2 diff --git a/.github/workflows/submit-winget.yml b/.github/workflows/submit-winget.yml index aa1c4dec..efa6cee7 100644 --- a/.github/workflows/submit-winget.yml +++ b/.github/workflows/submit-winget.yml @@ -11,6 +11,11 @@ jobs: runs-on: windows-latest if: ${{ !github.event.release.prerelease }} steps: + - name: Sync Winget Fork + run: gh repo sync ${{ secrets.WINGET_USERNAME }}/winget-pkgs -b master + env: + GH_TOKEN: ${{ secrets.WINGET_TOKEN }} + - name: Submit package to Windows Package Manager Community Repository run: | $wingetPackage = "Tautulli.Tautulli" @@ -23,3 +28,17 @@ jobs: # getting latest wingetcreate file iwr https://aka.ms/wingetcreate/latest -OutFile wingetcreate.exe .\wingetcreate.exe update $wingetPackage -s -v $version -u $installerUrl -t $gitToken + + virus-total: + name: VirusTotal Scan + runs-on: ubuntu-latest + steps: + - name: Upload to VirusTotal + uses: crazy-max/ghaction-virustotal@v4 + with: + vt_api_key: ${{ secrets.VT_API_KEY }} + github_token: ${{ secrets.GHACTIONS_TOKEN }} + update_release_body: true + files: | + .exe$ + .pkg$ diff --git a/CHANGELOG.md b/CHANGELOG.md index 1266e43f..b349b355 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,200 @@ # Changelog +## v2.15.3 (2025-08-03) + +* Exporter: + * New: Added hearingImpaired for subtitles and visualImpaired for audio attributes to exporter fields. +* Graphs: + * Fix: Remove duplicate "Total" entry in graph tooltips. (Thanks @zdimension) (#2534) +* UI: + * Fix: Failing to retrieve collections / playlists with over 1000 items. + * Fix: Scrollbar not showing on macosx and webkit browsers. (#2221) + * Fix: Incorrect rounding of minutes in global stats play duration. + * Fix: Disable browser autocomplete for notification agent and newsletter agent configurations. (#2557) +* API: + * New: Added ability to return svg files using pms_image_proxy API command. +* Other: + * New: Added ability to set config values using environment variables. (Thanks @komuw) (#2309, #2543) + + +## v2.15.2 (2025-04-12) + +* Activity: + * New: Added link to library by clicking media type icon. + * New: Added stream count to tab title on homepage. (#2517) +* History: + * Fix: Check stream watched status before stream stopped status. (#2506) +* Notifications: + * Fix: ntfy notifications failing to send if provider link is blank. + * Fix: Check Pushover notification attachment is under 5MB limit. (#2396) + * Fix: Track URLs redirecting to the correct media page. (#2513) + * New: Added audio profile notification parameters. + * New: Added PATCH method for Webhook notifications. +* Graphs: + * New: Added Total line to daily streams graph. (Thanks @zdimension) (#2497) +* UI: + * Fix: Do not redirect API requests to the login page. (#2490) + * Change: Swap source and stream columns in stream info modal. +* Other: + * Fix: Various typos. (Thanks @luzpaz) (#2520) + * Fix: CherryPy CORS response header not being set correctly. (#2279) + + +## v2.15.1 (2025-01-11) + +* Activity: + * Fix: Detection of HDR transcodes. (Thanks @cdecker08) (#2412, #2466) +* Newsletters: + * Fix: Disable basic authentication for /newsletter and /image endpoints. (#2472) +* Exporter: + * New: Added logos to season and episode exports. +* Other: + * Fix: Docker container https health check. + + +## v2.15.0 (2024-11-24) + +* Notes: + * Support for Python 3.8 has been dropped. The minimum Python version is now 3.9. +* Notifications: + * New: Allow Telegram blockquote and tg-emoji HTML tags. (Thanks @MythodeaLoL) (#2427) + * New: Added Plex slug and Plex Watch URL notification parameters. (#2420) + * Change: Update OneSignal API calls to use the new API endpoint for Tautulli Remote App notifications. +* Newsletters: + * Fix: Dumping custom dates in raw newsletter json. +* History: + * Fix: Unable to fix match for artists. (#2429) +* Exporter: + * New: Added movie and episode hasVoiceActivity attribute to exporter fields. + * New: Added subtitle canAutoSync attribute to exporter fields. + * New: Added logos to the exporter fields. +* UI: + * New: Add friendly name to the top bar of config modals. (Thanks @peagravel) (#2432) +* API: + * New: Added plex slugs to metadata in the get_metadata API command. +* Other: + * Fix: Tautulli failing to start with Python 3.13. (#2426) + + +## v2.14.6 (2024-10-12) + +* Newsletters: + * Fix: Allow formatting newsletter date parameters. + * Change: Support apscheduler compatible cron expressions. +* UI: + * Fix: Round runtime before converting to human duration. + * Fix: Make recently added/watched rows touch scrollable. +* Other: + * Fix: Auto-updater not running. + + +## v2.14.5 (2024-09-20) + +* Activity: + * Fix: Display of 2k resolution on activity card. +* Notifications: + * Fix: ntfy notifications with special characters failing to send. +* Other: + * Fix: Memory leak with database closing. (#2404) + + +## v2.14.4 (2024-08-10) + +* Notifications: + * Fix: Update Slack notification info card. + * New: Added ntfy notification agent. (Thanks @nwithan8) (#2356, #2000) +* UI: + * Fix: macOS platform capitalization. +* Other: + * Fix: Remove deprecated getdefaultlocale. (Thanks @teodorstelian) (#2364, #2345) + + +## v2.14.3 (2024-06-19) + +* Graphs: + * Fix: History table not loading when clicking on the graphs in some instances. +* UI: + * Fix: Scheduled tasks table not loading when certain tasks are disabled. + * Removed: Unnecessary Remote Server checkbox from the settings page. +* Other: + * Fix: Webserver not restarting after the setup wizard. + * Fix: Workaround webserver crashing in some instances. + + +## v2.14.2 (2024-05-18) + +* History: + * Fix: Live TV activity not logging to history. + * Fix: Incorrect grouping of live TV history. +* Notifications: + * Fix: Pushover configuration settings refreshing after entering a token. + * Fix: Plex remote access down notifications not triggering. + * Fix: Deleting all images from Cloudinary only deleting 1000 images. + * New: Added platform version and product version notification parameters. (#2244) + * New: Added LAN streams and WAN streams notification parameters. (#2276) + * New: Added Dolby Vision notification parameters. (#2240) + * New: Added live TV channel notification parameters. + * Change: Improved Tautulli Remote App notification encryption method. + * Note: Requires Tautulli Remote App version 3.2.4. +* Exporter: + * New: Added slug attribute to exporter fields. + * New: Added track genres to exporter fields. + * New: Added playlist source URI to exporter fields. + * New: Added artProvider and thumbProvider to exporter fields. +* UI: + * Fix: Mask deleted usernames in the logs. + * Fix: Live TV watch stats not showing on the media info page. + * Fix: Users without access to Plex server not showing as inactive. + * Removed: Deprecated synced item pages. + * Removed: Anonymous redirect settings. Links now use browser no-referrer policy instead. +* API: + * New: Added Dolby Vision info to the get_metadata API command. + * New: Added before and after parameters to the get_home_stats API command. (#2231) +* Packages: + * New: Universal binary for macOS for Apple silicon. + * New: Bump Snap package to core22. +* Other: + * Change: Login cookie expires changed to max-age. + * Change: Improved key generation for login password. It is recommended to reenter your HTTP Password in the settings after upgrading. + * Removed: Python 2 compatibility code. (#2098, #2226) (Thanks @zdimension) + + +## v2.13.4 (2023-12-07) + +* UI: + * Fix: Tautulli configuration settings page not loading when system language is None. + * Fix: Login cookie expiring too quickly. + + +## v2.13.3 (2023-12-03) + +* Notifications: + * New: Added duration_time notification parameter. + * New: Added file_size_bytes notification parameter. + * New: Added time formats notification text modifiers. + * New: Added support for thetvdb_url for movies. +* UI: + * Fix: Activity card overflowing due to screen scaling. (#2033) + * Fix: Stream duration on activity card not being updated on track changes in some cases. (#2206) + + +## v2.13.2 (2023-10-26) + +* History: + * New: Added quarter values icons for history watch status. (#2179, #2156) (Thanks @herby2212) +* Graphs: + * New: Added concurrent streams per day graph. (#2046) (Thanks @herby2212) +* Exporter: + * New: Added metadata directory to exporter fields. + * Removed: Banner exporter fields for tv shows. +* UI: + * New: Added last triggered time to notification agents and newsletter agent lists. +* Other: + * New: Added X-Plex-Language header override to config file. + + ## v2.13.1 (2023-08-25) + * Notes: * Support for Python 3.7 has been dropped. The minimum Python version is now 3.8. * Other: diff --git a/Dockerfile b/Dockerfile index 7a52841f..8d8c324b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,4 +25,4 @@ CMD [ "python", "Tautulli.py", "--datadir", "/config" ] ENTRYPOINT [ "./start.sh" ] 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 +HEALTHCHECK --start-period=90s CMD curl -ILfks https://localhost:8181/status > /dev/null || curl -ILfs http://localhost:8181/status > /dev/null || exit 1 diff --git a/README.md b/README.md index a6a3e4e4..37829290 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ and [PlexWatchWeb](https://github.com/ecleese/plexWatchWeb). [![Docker Stars][badge-docker-stars]][DockerHub] [![Downloads][badge-downloads]][Releases Latest] -[badge-python]: https://img.shields.io/badge/python->=3.8-blue?style=flat-square +[badge-python]: https://img.shields.io/badge/python->=3.9-blue?style=flat-square [badge-docker-pulls]: https://img.shields.io/docker/pulls/tautulli/tautulli?style=flat-square [badge-docker-stars]: https://img.shields.io/docker/stars/tautulli/tautulli?style=flat-square [badge-downloads]: https://img.shields.io/github/downloads/Tautulli/Tautulli/total?style=flat-square @@ -129,7 +129,7 @@ This is free software under the GPL v3 open source license. Feel free to do with 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 +non-commercial use. Commercial users must licence this software, for more information visit https://shop.highsoft.com/faq/non-commercial#non-commercial-redistribution. diff --git a/Tautulli.py b/Tautulli.py index eebfa55a..b3cf4736 100755 --- a/Tautulli.py +++ b/Tautulli.py @@ -23,18 +23,18 @@ import sys # Ensure lib added to path, before any other imports sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'lib')) -from future.builtins import str -import appdirs import argparse import datetime import locale +import platformdirs import pytz import signal import shutil import time import threading import tzlocal +import ctypes import plexpy from plexpy import common, config, database, helpers, logger, webstart @@ -70,8 +70,26 @@ def main(): plexpy.SYS_ENCODING = None try: - locale.setlocale(locale.LC_ALL, "") - plexpy.SYS_LANGUAGE, plexpy.SYS_ENCODING = locale.getdefaultlocale() + + # Attempt to get the system's locale settings + language_code, encoding = locale.getlocale() + + # Special handling for Windows platform + if sys.platform == 'win32': + # Get the user's current language settings on Windows + windll = ctypes.windll.kernel32 + lang_id = windll.GetUserDefaultLCID() + + # Map Windows language ID to locale identifier + language_code = locale.windows_locale.get(lang_id, '') + + # Get the preferred encoding + encoding = locale.getpreferredencoding() + + # Assign values to application-specific variable + plexpy.SYS_LANGUAGE = language_code + plexpy.SYS_ENCODING = encoding + except (locale.Error, IOError): pass @@ -111,7 +129,7 @@ def main(): if args.quiet: plexpy.QUIET = True - # Do an intial setup of the logger. + # Do an initial setup of the logger. # Require verbose for pre-initilization to see critical errors logger.initLogger(console=not plexpy.QUIET, log_dir=False, verbose=True) @@ -186,7 +204,7 @@ def main(): if args.datadir: plexpy.DATA_DIR = args.datadir elif plexpy.FROZEN: - plexpy.DATA_DIR = appdirs.user_data_dir("Tautulli", False) + plexpy.DATA_DIR = platformdirs.user_data_dir("Tautulli", False) else: plexpy.DATA_DIR = plexpy.PROG_DIR diff --git a/data/interfaces/default/base.html b/data/interfaces/default/base.html index 19edb94b..d6c9f859 100644 --- a/data/interfaces/default/base.html +++ b/data/interfaces/default/base.html @@ -13,6 +13,7 @@ + @@ -123,11 +124,6 @@ % else:
  • Graphs
  • % endif - % if title == "Synced Items": -
  • Synced Items
  • - % else: -
  • Synced Items
  • - % endif % if title == "Settings":
  • Patreon
  • Stripe
  • PayPal
  • -
  • Crypto
  • +
  • Crypto
  • @@ -287,7 +283,16 @@ ${next.modalIncludes()}

    - Click the button below to continue to Coinbase. + Select a cryptocurrency. +

    + +
    +
    + + +
    +

    + Or click the button below to continue to Coinbase.

    + ${row['title']} + % endif
    diff --git a/data/interfaces/default/index.html b/data/interfaces/default/index.html index 57236e69..ca95be42 100644 --- a/data/interfaces/default/index.html +++ b/data/interfaces/default/index.html @@ -92,10 +92,10 @@

    Recently Added

    @@ -212,28 +212,6 @@
    -<% from plexpy.helpers import anon_url %> - % endif
    - % if media_info['channel_identifier']: - Channel ${media_info['channel_call_sign']} ${media_info['channel_identifier']} + % if media_info['channel_vcn']: + Channel ${media_info['channel_title'] or (media_info['channel_vcn'] + ' ' + media_info['channel_call_sign'])} % endif
    @@ -878,7 +878,7 @@ DOCUMENTATION :: END transcode_decision: transcode_decision, user_id: "${history_user_id}", % if data['live']: - guid: "${data['guid']} + guid: "${data['guid']}" % elif data['media_type'] in ('show', 'artist'): grandparent_rating_key: "${data['rating_key']}" % elif data['media_type'] in ('season', 'album'): @@ -947,8 +947,12 @@ DOCUMENTATION :: END url: 'item_watch_time_stats', async: true, data: { + % if data['live']: + guid: "${data['guid']}" + % else: rating_key: "${data['rating_key']}", - media_type: "${data['media_type']}" + media_type: "${data['media_type']}" + % endif }, complete: function(xhr, status) { $("#watch-time-stats").html(xhr.responseText); @@ -959,8 +963,12 @@ DOCUMENTATION :: END url: 'item_user_stats', async: true, data: { + % if data['live']: + guid: "${data['guid']}" + % else: rating_key: "${data['rating_key']}", - media_type: "${data['media_type']}" + media_type: "${data['media_type']}" + % endif }, complete: function(xhr, status) { $("#user-stats").html(xhr.responseText); diff --git a/data/interfaces/default/js/graphs/concurrent_streams_by_stream_type.js b/data/interfaces/default/js/graphs/concurrent_streams_by_stream_type.js new file mode 100644 index 00000000..623e4735 --- /dev/null +++ b/data/interfaces/default/js/graphs/concurrent_streams_by_stream_type.js @@ -0,0 +1,76 @@ +var formatter_function = function() { + if (moment(this.x, 'X').isValid() && (this.x > 946684800)) { + var s = ''+ moment(this.x).format('ddd MMM D') +''; + } else { + var s = ''+ this.x +''; + } + $.each(this.points, function(i, point) { + s += '
    '+point.series.name+': '+point.y; + }); + return s; +}; + +var hc_concurrent_streams_by_stream_type_options = { + chart: { + type: 'line', + backgroundColor: 'rgba(0,0,0,0)', + renderTo: 'graph_concurrent_streams_by_stream_type' + }, + title: { + text: '' + }, + legend: { + enabled: true, + itemStyle: { + font: '9pt "Open Sans", sans-serif', + color: '#A0A0A0' + }, + itemHoverStyle: { + color: '#FFF' + }, + itemHiddenStyle: { + color: '#444' + } + }, + credits: { + enabled: false + }, + plotOptions: { + series: { + events: { + legendItemClick: function() { + setGraphVisibility(this.chart.renderTo.id, this.chart.series, this.name); + } + } + } + }, + xAxis: { + type: 'datetime', + labels: { + formatter: function() { + return moment(this.value).format("MMM D"); + }, + style: { + color: '#aaa' + } + }, + categories: [{}], + plotBands: [] + }, + yAxis: { + title: { + text: null + }, + labels: { + style: { + color: '#aaa' + } + } + }, + tooltip: { + shared: true, + crosshairs: true, + formatter: formatter_function + }, + series: [{}] +}; \ No newline at end of file diff --git a/data/interfaces/default/js/jquery.scrollbar.min.js b/data/interfaces/default/js/jquery.scrollbar.min.js index 5a86f9ad..cf9d8935 100644 --- a/data/interfaces/default/js/jquery.scrollbar.min.js +++ b/data/interfaces/default/js/jquery.scrollbar.min.js @@ -13,4 +13,4 @@ * @url https://github.com/gromo/jquery.scrollbar/ * */ -!function(a,b){"function"==typeof define&&define.amd?define(["jquery"],b):b("undefined"!=typeof exports?require("jquery"):a.jQuery)}(this,function(a){"use strict";function h(b){if(c.webkit&&!b)return{height:0,width:0};if(!c.data.outer){var d={border:"none","box-sizing":"content-box",height:"200px",margin:"0",padding:"0",width:"200px"};c.data.inner=a("
    ").css(a.extend({},d)),c.data.outer=a("
    ").css(a.extend({left:"-1000px",overflow:"scroll",position:"absolute",top:"-1000px"},d)).append(c.data.inner).appendTo("body")}return c.data.outer.scrollLeft(1e3).scrollTop(1e3),{height:Math.ceil(c.data.outer.offset().top-c.data.inner.offset().top||0),width:Math.ceil(c.data.outer.offset().left-c.data.inner.offset().left||0)}}function i(){var a=h(!0);return!(a.height||a.width)}function j(a){var b=a.originalEvent;return(!b.axis||b.axis!==b.HORIZONTAL_AXIS)&&!b.wheelDeltaX}var b=!1,c={data:{index:0,name:"scrollbar"},firefox:/firefox/i.test(navigator.userAgent),macosx:/mac/i.test(navigator.platform),msedge:/edge\/\d+/i.test(navigator.userAgent),msie:/(msie|trident)/i.test(navigator.userAgent),mobile:/android|webos|iphone|ipad|ipod|blackberry/i.test(navigator.userAgent),overlay:null,scroll:null,scrolls:[],webkit:/webkit/i.test(navigator.userAgent)&&!/edge\/\d+/i.test(navigator.userAgent)};c.scrolls.add=function(a){this.remove(a).push(a)},c.scrolls.remove=function(b){for(;a.inArray(b,this)>=0;)this.splice(a.inArray(b,this),1);return this};var d={autoScrollSize:!0,autoUpdate:!0,debug:!1,disableBodyScroll:!1,duration:200,ignoreMobile:!1,ignoreOverlay:!1,isRtl:!1,scrollStep:30,showArrows:!1,stepScrolling:!0,scrollx:null,scrolly:null,onDestroy:null,onFallback:null,onInit:null,onScroll:null,onUpdate:null},e=function(b){c.scroll||(c.overlay=i(),c.scroll=h(),g(),a(window).resize(function(){var a=!1;if(c.scroll&&(c.scroll.height||c.scroll.width)){var b=h();b.height===c.scroll.height&&b.width===c.scroll.width||(c.scroll=b,a=!0)}g(a)})),this.container=b,this.namespace=".scrollbar_"+c.data.index++,this.options=a.extend({},d,window.jQueryScrollbarOptions||{}),this.scrollTo=null,this.scrollx={},this.scrolly={},b.data(c.data.name,this),c.scrolls.add(this)};e.prototype={destroy:function(){if(this.wrapper){this.container.removeData(c.data.name),c.scrolls.remove(this);var b=this.container.scrollLeft(),d=this.container.scrollTop();this.container.insertBefore(this.wrapper).css({height:"",margin:"","max-height":""}).removeClass("scroll-content scroll-scrollx_visible scroll-scrolly_visible").off(this.namespace).scrollLeft(b).scrollTop(d),this.scrollx.scroll.removeClass("scroll-scrollx_visible").find("div").addBack().off(this.namespace),this.scrolly.scroll.removeClass("scroll-scrolly_visible").find("div").addBack().off(this.namespace),this.wrapper.remove(),a(document).add("body").off(this.namespace),a.isFunction(this.options.onDestroy)&&this.options.onDestroy.apply(this,[this.container])}},init:function(b){var d=this,e=this.container,f=this.containerWrapper||e,g=this.namespace,h=a.extend(this.options,b||{}),i={x:this.scrollx,y:this.scrolly},k=this.wrapper,l={},m={scrollLeft:e.scrollLeft(),scrollTop:e.scrollTop()};if(c.mobile&&h.ignoreMobile||c.overlay&&h.ignoreOverlay||c.macosx&&!c.webkit)return a.isFunction(h.onFallback)&&h.onFallback.apply(this,[e]),!1;if(k)l={height:"auto","margin-bottom":c.scroll.height*-1+"px","max-height":""},l[h.isRtl?"margin-left":"margin-right"]=c.scroll.width*-1+"px",f.css(l);else{if(this.wrapper=k=a("
    ").addClass("scroll-wrapper").addClass(e.attr("class")).css("position","absolute"===e.css("position")?"absolute":"relative").insertBefore(e).append(e),h.isRtl&&k.addClass("scroll--rtl"),e.is("textarea")&&(this.containerWrapper=f=a("
    ").insertBefore(e).append(e),k.addClass("scroll-textarea")),l={height:"auto","margin-bottom":c.scroll.height*-1+"px","max-height":""},l[h.isRtl?"margin-left":"margin-right"]=c.scroll.width*-1+"px",f.addClass("scroll-content").css(l),e.on("scroll"+g,function(b){var f=e.scrollLeft(),g=e.scrollTop();if(h.isRtl)switch(!0){case c.firefox:f=Math.abs(f);case c.msedge||c.msie:f=e[0].scrollWidth-e[0].clientWidth-f}a.isFunction(h.onScroll)&&h.onScroll.call(d,{maxScroll:i.y.maxScrollOffset,scroll:g,size:i.y.size,visible:i.y.visible},{maxScroll:i.x.maxScrollOffset,scroll:f,size:i.x.size,visible:i.x.visible}),i.x.isVisible&&i.x.scroll.bar.css("left",f*i.x.kx+"px"),i.y.isVisible&&i.y.scroll.bar.css("top",g*i.y.kx+"px")}),k.on("scroll"+g,function(){k.scrollTop(0).scrollLeft(0)}),h.disableBodyScroll){var n=function(a){j(a)?i.y.isVisible&&i.y.mousewheel(a):i.x.isVisible&&i.x.mousewheel(a)};k.on("MozMousePixelScroll"+g,n),k.on("mousewheel"+g,n),c.mobile&&k.on("touchstart"+g,function(b){var c=b.originalEvent.touches&&b.originalEvent.touches[0]||b,d={pageX:c.pageX,pageY:c.pageY},f={left:e.scrollLeft(),top:e.scrollTop()};a(document).on("touchmove"+g,function(a){var b=a.originalEvent.targetTouches&&a.originalEvent.targetTouches[0]||a;e.scrollLeft(f.left+d.pageX-b.pageX),e.scrollTop(f.top+d.pageY-b.pageY),a.preventDefault()}),a(document).on("touchend"+g,function(){a(document).off(g)})})}a.isFunction(h.onInit)&&h.onInit.apply(this,[e])}a.each(i,function(b,f){var k=null,l=1,m="x"===b?"scrollLeft":"scrollTop",n=h.scrollStep,o=function(){var a=e[m]();e[m](a+n),1==l&&a+n>=p&&(a=e[m]()),l==-1&&a+n<=p&&(a=e[m]()),e[m]()==a&&k&&k()},p=0;f.scroll||(f.scroll=d._getScroll(h["scroll"+b]).addClass("scroll-"+b),h.showArrows&&f.scroll.addClass("scroll-element_arrows_visible"),f.mousewheel=function(a){if(!f.isVisible||"x"===b&&j(a))return!0;if("y"===b&&!j(a))return i.x.mousewheel(a),!0;var c=a.originalEvent.wheelDelta*-1||a.originalEvent.detail,g=f.size-f.visible-f.offset;return c||("x"===b&&a.originalEvent.deltaX?c=40*a.originalEvent.deltaX:"y"===b&&a.originalEvent.deltaY&&(c=40*a.originalEvent.deltaY)),(c>0&&p0)&&(p+=c,p<0&&(p=0),p>g&&(p=g),d.scrollTo=d.scrollTo||{},d.scrollTo[m]=p,setTimeout(function(){d.scrollTo&&(e.stop().animate(d.scrollTo,240,"linear",function(){p=e[m]()}),d.scrollTo=null)},1)),a.preventDefault(),!1},f.scroll.on("MozMousePixelScroll"+g,f.mousewheel).on("mousewheel"+g,f.mousewheel).on("mouseenter"+g,function(){p=e[m]()}),f.scroll.find(".scroll-arrow, .scroll-element_track").on("mousedown"+g,function(g){if(1!=g.which)return!0;l=1;var i={eventOffset:g["x"===b?"pageX":"pageY"],maxScrollValue:f.size-f.visible-f.offset,scrollbarOffset:f.scroll.bar.offset()["x"===b?"left":"top"],scrollbarSize:f.scroll.bar["x"===b?"outerWidth":"outerHeight"]()},j=0,q=0;if(a(this).hasClass("scroll-arrow")){if(l=a(this).hasClass("scroll-arrow_more")?1:-1,n=h.scrollStep*l,p=l>0?i.maxScrollValue:0,h.isRtl)switch(!0){case c.firefox:p=l>0?0:i.maxScrollValue*-1;break;case c.msie||c.msedge:}}else l=i.eventOffset>i.scrollbarOffset+i.scrollbarSize?1:i.eventOffset','
    ','
    ','
    ','
    ','
    ','
    ','
    ','
    ',"
    ","
    ",'
    ','
    ','
    ',"
    ",'
    ','
    ',"
    ","
    ","
    "].join(""),simple:['
    ','
    ','
    ','
    ','
    ',"
    ","
    "].join("")};return c[b]&&(b=c[b]),b||(b=c.simple),b="string"==typeof b?a(b).appendTo(this.wrapper):a(b),a.extend(b,{bar:b.find(".scroll-bar"),size:b.find(".scroll-element_size"),track:b.find(".scroll-element_track")}),b},_handleMouseDown:function(b,c){var d=this.namespace;return a(document).on("blur"+d,function(){a(document).add("body").off(d),b&&b()}),a(document).on("dragstart"+d,function(a){return a.preventDefault(),!1}),a(document).on("mouseup"+d,function(){a(document).add("body").off(d),b&&b()}),a("body").on("selectstart"+d,function(a){return a.preventDefault(),!1}),c&&c.preventDefault(),!1},_updateScroll:function(b,d){var e=this.container,f=this.containerWrapper||e,g="scroll-scroll"+b+"_visible",h="x"===b?this.scrolly:this.scrollx,i=parseInt(this.container.css("x"===b?"left":"top"),10)||0,j=this.wrapper,k=d.size,l=d.visible+i;d.isVisible=k-l>1,d.isVisible?(d.scroll.addClass(g),h.scroll.addClass(g),f.addClass(g)):(d.scroll.removeClass(g),h.scroll.removeClass(g),f.removeClass(g)),"y"===b&&(e.is("textarea")||k10?(window.console&&console.log("Scroll updates exceed 10"),g=function(){}):(clearTimeout(a),a=setTimeout(g,300))}}();window.angular&&!function(a){a.module("jQueryScrollbar",[]).provider("jQueryScrollbar",function(){var b=d;return{setOptions:function(c){a.extend(b,c)},$get:function(){return{options:a.copy(b)}}}}).directive("jqueryScrollbar",["jQueryScrollbar","$parse",function(a,b){return{restrict:"AC",link:function(c,d,e){var f=b(e.jqueryScrollbar),g=f(c);d.scrollbar(g||a.options).on("$destroy",function(){d.scrollbar("destroy")})}}}])}(window.angular)}); \ No newline at end of file +!function(a,b){"function"==typeof define&&define.amd?define(["jquery"],b):b("undefined"!=typeof exports?require("jquery"):a.jQuery)}(this,function(a){"use strict";function h(b){if(c.webkit&&!b)return{height:0,width:0};if(!c.data.outer){var d={border:"none","box-sizing":"content-box",height:"200px",margin:"0",padding:"0",width:"200px"};c.data.inner=a("
    ").css(a.extend({},d)),c.data.outer=a("
    ").css(a.extend({left:"-1000px",overflow:"scroll",position:"absolute",top:"-1000px"},d)).append(c.data.inner).appendTo("body")}return c.data.outer.scrollLeft(1e3).scrollTop(1e3),{height:Math.ceil(c.data.outer.offset().top-c.data.inner.offset().top||0),width:Math.ceil(c.data.outer.offset().left-c.data.inner.offset().left||0)}}function i(){var a=h(!0);return!(a.height||a.width)}function j(a){var b=a.originalEvent;return(!b.axis||b.axis!==b.HORIZONTAL_AXIS)&&!b.wheelDeltaX}var b=!1,c={data:{index:0,name:"scrollbar"},firefox:/firefox/i.test(navigator.userAgent),macosx:/mac/i.test(navigator.platform),msedge:/edge\/\d+/i.test(navigator.userAgent),msie:/(msie|trident)/i.test(navigator.userAgent),mobile:/android|webos|iphone|ipad|ipod|blackberry/i.test(navigator.userAgent),overlay:null,scroll:null,scrolls:[],webkit:/webkit/i.test(navigator.userAgent)&&!/edge\/\d+/i.test(navigator.userAgent)};c.scrolls.add=function(a){this.remove(a).push(a)},c.scrolls.remove=function(b){for(;a.inArray(b,this)>=0;)this.splice(a.inArray(b,this),1);return this};var d={autoScrollSize:!0,autoUpdate:!0,debug:!1,disableBodyScroll:!1,duration:200,ignoreMobile:!1,ignoreOverlay:!1,isRtl:!1,scrollStep:30,showArrows:!1,stepScrolling:!0,scrollx:null,scrolly:null,onDestroy:null,onFallback:null,onInit:null,onScroll:null,onUpdate:null},e=function(b){c.scroll||(c.overlay=i(),c.scroll=h(),g(),a(window).resize(function(){var a=!1;if(c.scroll&&(c.scroll.height||c.scroll.width)){var b=h();b.height===c.scroll.height&&b.width===c.scroll.width||(c.scroll=b,a=!0)}g(a)})),this.container=b,this.namespace=".scrollbar_"+c.data.index++,this.options=a.extend({},d,window.jQueryScrollbarOptions||{}),this.scrollTo=null,this.scrollx={},this.scrolly={},b.data(c.data.name,this),c.scrolls.add(this)};e.prototype={destroy:function(){if(this.wrapper){this.container.removeData(c.data.name),c.scrolls.remove(this);var b=this.container.scrollLeft(),d=this.container.scrollTop();this.container.insertBefore(this.wrapper).css({height:"",margin:"","max-height":""}).removeClass("scroll-content scroll-scrollx_visible scroll-scrolly_visible").off(this.namespace).scrollLeft(b).scrollTop(d),this.scrollx.scroll.removeClass("scroll-scrollx_visible").find("div").addBack().off(this.namespace),this.scrolly.scroll.removeClass("scroll-scrolly_visible").find("div").addBack().off(this.namespace),this.wrapper.remove(),a(document).add("body").off(this.namespace),a.isFunction(this.options.onDestroy)&&this.options.onDestroy.apply(this,[this.container])}},init:function(b){var d=this,e=this.container,f=this.containerWrapper||e,g=this.namespace,h=a.extend(this.options,b||{}),i={x:this.scrollx,y:this.scrolly},k=this.wrapper,l={},m={scrollLeft:e.scrollLeft(),scrollTop:e.scrollTop()};if(c.mobile&&h.ignoreMobile||c.overlay&&h.ignoreOverlay)return a.isFunction(h.onFallback)&&h.onFallback.apply(this,[e]),!1;if(k)l={height:"auto","margin-bottom":c.scroll.height*-1+"px","max-height":""},l[h.isRtl?"margin-left":"margin-right"]=c.scroll.width*-1+"px",f.css(l);else{if(this.wrapper=k=a("
    ").addClass("scroll-wrapper").addClass(e.attr("class")).css("position","absolute"===e.css("position")?"absolute":"relative").insertBefore(e).append(e),h.isRtl&&k.addClass("scroll--rtl"),e.is("textarea")&&(this.containerWrapper=f=a("
    ").insertBefore(e).append(e),k.addClass("scroll-textarea")),l={height:"auto","margin-bottom":c.scroll.height*-1+"px","max-height":""},l[h.isRtl?"margin-left":"margin-right"]=c.scroll.width*-1+"px",f.addClass("scroll-content").css(l),e.on("scroll"+g,function(b){var f=e.scrollLeft(),g=e.scrollTop();if(h.isRtl)switch(!0){case c.firefox:f=Math.abs(f);case c.msedge||c.msie:f=e[0].scrollWidth-e[0].clientWidth-f}a.isFunction(h.onScroll)&&h.onScroll.call(d,{maxScroll:i.y.maxScrollOffset,scroll:g,size:i.y.size,visible:i.y.visible},{maxScroll:i.x.maxScrollOffset,scroll:f,size:i.x.size,visible:i.x.visible}),i.x.isVisible&&i.x.scroll.bar.css("left",f*i.x.kx+"px"),i.y.isVisible&&i.y.scroll.bar.css("top",g*i.y.kx+"px")}),k.on("scroll"+g,function(){k.scrollTop(0).scrollLeft(0)}),h.disableBodyScroll){var n=function(a){j(a)?i.y.isVisible&&i.y.mousewheel(a):i.x.isVisible&&i.x.mousewheel(a)};k.on("MozMousePixelScroll"+g,n),k.on("mousewheel"+g,n),c.mobile&&k.on("touchstart"+g,function(b){var c=b.originalEvent.touches&&b.originalEvent.touches[0]||b,d={pageX:c.pageX,pageY:c.pageY},f={left:e.scrollLeft(),top:e.scrollTop()};a(document).on("touchmove"+g,function(a){var b=a.originalEvent.targetTouches&&a.originalEvent.targetTouches[0]||a;e.scrollLeft(f.left+d.pageX-b.pageX),e.scrollTop(f.top+d.pageY-b.pageY),a.preventDefault()}),a(document).on("touchend"+g,function(){a(document).off(g)})})}a.isFunction(h.onInit)&&h.onInit.apply(this,[e])}a.each(i,function(b,f){var k=null,l=1,m="x"===b?"scrollLeft":"scrollTop",n=h.scrollStep,o=function(){var a=e[m]();e[m](a+n),1==l&&a+n>=p&&(a=e[m]()),l==-1&&a+n<=p&&(a=e[m]()),e[m]()==a&&k&&k()},p=0;f.scroll||(f.scroll=d._getScroll(h["scroll"+b]).addClass("scroll-"+b),h.showArrows&&f.scroll.addClass("scroll-element_arrows_visible"),f.mousewheel=function(a){if(!f.isVisible||"x"===b&&j(a))return!0;if("y"===b&&!j(a))return i.x.mousewheel(a),!0;var c=a.originalEvent.wheelDelta*-1||a.originalEvent.detail,g=f.size-f.visible-f.offset;return c||("x"===b&&a.originalEvent.deltaX?c=40*a.originalEvent.deltaX:"y"===b&&a.originalEvent.deltaY&&(c=40*a.originalEvent.deltaY)),(c>0&&p0)&&(p+=c,p<0&&(p=0),p>g&&(p=g),d.scrollTo=d.scrollTo||{},d.scrollTo[m]=p,setTimeout(function(){d.scrollTo&&(e.stop().animate(d.scrollTo,240,"linear",function(){p=e[m]()}),d.scrollTo=null)},1)),a.preventDefault(),!1},f.scroll.on("MozMousePixelScroll"+g,f.mousewheel).on("mousewheel"+g,f.mousewheel).on("mouseenter"+g,function(){p=e[m]()}),f.scroll.find(".scroll-arrow, .scroll-element_track").on("mousedown"+g,function(g){if(1!=g.which)return!0;l=1;var i={eventOffset:g["x"===b?"pageX":"pageY"],maxScrollValue:f.size-f.visible-f.offset,scrollbarOffset:f.scroll.bar.offset()["x"===b?"left":"top"],scrollbarSize:f.scroll.bar["x"===b?"outerWidth":"outerHeight"]()},j=0,q=0;if(a(this).hasClass("scroll-arrow")){if(l=a(this).hasClass("scroll-arrow_more")?1:-1,n=h.scrollStep*l,p=l>0?i.maxScrollValue:0,h.isRtl)switch(!0){case c.firefox:p=l>0?0:i.maxScrollValue*-1;break;case c.msie||c.msedge:}}else l=i.eventOffset>i.scrollbarOffset+i.scrollbarSize?1:i.eventOffset','
    ','
    ','
    ','
    ','
    ','
    ','
    ','
    ',"
    ","
    ",'
    ','
    ','
    ',"
    ",'
    ','
    ',"
    ","
    ","
    "].join(""),simple:['
    ','
    ','
    ','
    ','
    ',"
    ","
    "].join("")};return c[b]&&(b=c[b]),b||(b=c.simple),b="string"==typeof b?a(b).appendTo(this.wrapper):a(b),a.extend(b,{bar:b.find(".scroll-bar"),size:b.find(".scroll-element_size"),track:b.find(".scroll-element_track")}),b},_handleMouseDown:function(b,c){var d=this.namespace;return a(document).on("blur"+d,function(){a(document).add("body").off(d),b&&b()}),a(document).on("dragstart"+d,function(a){return a.preventDefault(),!1}),a(document).on("mouseup"+d,function(){a(document).add("body").off(d),b&&b()}),a("body").on("selectstart"+d,function(a){return a.preventDefault(),!1}),c&&c.preventDefault(),!1},_updateScroll:function(b,d){var e=this.container,f=this.containerWrapper||e,g="scroll-scroll"+b+"_visible",h="x"===b?this.scrolly:this.scrollx,i=parseInt(this.container.css("x"===b?"left":"top"),10)||0,j=this.wrapper,k=d.size,l=d.visible+i;d.isVisible=k-l>1,d.isVisible?(d.scroll.addClass(g),h.scroll.addClass(g),f.addClass(g)):(d.scroll.removeClass(g),h.scroll.removeClass(g),f.removeClass(g)),"y"===b&&(e.is("textarea")||k10?(window.console&&console.log("Scroll updates exceed 10"),g=function(){}):(clearTimeout(a),a=setTimeout(g,300))}}();window.angular&&!function(a){a.module("jQueryScrollbar",[]).provider("jQueryScrollbar",function(){var b=d;return{setOptions:function(c){a.extend(b,c)},$get:function(){return{options:a.copy(b)}}}}).directive("jqueryScrollbar",["jQueryScrollbar","$parse",function(a,b){return{restrict:"AC",link:function(c,d,e){var f=b(e.jqueryScrollbar),g=f(c);d.scrollbar(g||a.options).on("$destroy",function(){d.scrollbar("destroy")})}}}])}(window.angular)}); \ No newline at end of file diff --git a/data/interfaces/default/js/script.js b/data/interfaces/default/js/script.js index 2df5daeb..9f2a92eb 100644 --- a/data/interfaces/default/js/script.js +++ b/data/interfaces/default/js/script.js @@ -288,23 +288,10 @@ function isPrivateIP(ip_address) { } function humanTime(seconds) { - var d = Math.floor(moment.duration(seconds, 'seconds').asDays()); - var h = Math.floor(moment.duration((seconds % 86400), 'seconds').asHours()); - var m = Math.round(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()); - - var text = ''; - if (d > 0) { - text = '

    ' + d + '

    day' + ((d > 1) ? 's' : '') + '

    ' - + '

    ' + h + '

    hr' + ((h > 1) ? 's' : '') + '

    ' - + '

    ' + m + '

    min' + ((m > 1) ? 's' : '') + '

    '; - } else if (h > 0) { - text = '

    ' + h + '

    hr' + ((h > 1) ? 's' : '') + '

    ' - + '

    ' + m + '

    min' + ((m > 1) ? 's' : '') + '

    '; - } else { - text = '

    ' + m + '

    min' + ((m > 1) ? 's' : '') + '

    '; + if (seconds > 0) { + return humanDuration(seconds * 1000).replaceAll(/(\d+) (\w+)/g, '

    $1

    $2

    ') } - - return text + return "

    0

    mins

    "; } String.prototype.toProperCase = function () { @@ -360,7 +347,8 @@ function humanDuration(ms, sig='dhm', units='ms', return_seconds=300000) { sig = 'dhms' } - ms = ms * factors[units]; + r = factors[sig.slice(-1)]; + ms = Math.round(ms * factors[units] / r) * r; h = ms % factors['d']; d = Math.trunc(ms / factors['d']); @@ -929,3 +917,50 @@ $('.modal').on('hide.bs.modal', function (e) { $.fn.hasScrollBar = function() { return this.get(0).scrollHeight > this.get(0).clientHeight; } + +function paginateScroller(scrollerId, buttonClass) { + $(buttonClass).click(function (e) { + e.preventDefault(); + var scroller = $(scrollerId + "-row-scroller"); + var scrollerParent = scroller.parent(); + var containerWidth = scrollerParent.width(); + var scrollCurrent = scrollerParent.scrollLeft(); + var scrollAmount = $(this).data("id") * parseInt(containerWidth / 175) * 175; + var scrollMax = scroller.width() - Math.abs(scrollAmount); + var scrollTotal = Math.min(parseInt(scrollCurrent / 175) * 175 + scrollAmount, scrollMax); + scrollerParent.animate({ scrollLeft: scrollTotal }, 250); + }); +} + +function highlightScrollerButton(scrollerId) { + var scroller = $(scrollerId + "-row-scroller"); + var scrollerParent = scroller.parent(); + var buttonLeft = $(scrollerId + "-page-left"); + var buttonRight = $(scrollerId + "-page-right"); + + var numElems = scroller.find("li").length; + scroller.width(numElems * 175); + $(buttonLeft).addClass("disabled").blur(); + if (scroller.width() > scrollerParent.width()) { + $(buttonRight).removeClass("disabled"); + } else { + $(buttonRight).addClass("disabled"); + } + + scrollerParent.scroll(function () { + var scrollCurrent = $(this).scrollLeft(); + var scrollMax = scroller.width() - $(this).width(); + + if (scrollCurrent == 0) { + $(buttonLeft).addClass("disabled").blur(); + } else { + $(buttonLeft).removeClass("disabled"); + } + + if (scrollCurrent >= scrollMax) { + $(buttonRight).addClass("disabled").blur(); + } else { + $(buttonRight).removeClass("disabled"); + } + }); +} diff --git a/data/interfaces/default/js/tables/export_table.js b/data/interfaces/default/js/tables/export_table.js index 44fe4e13..c0f6cf2f 100644 --- a/data/interfaces/default/js/tables/export_table.js +++ b/data/interfaces/default/js/tables/export_table.js @@ -100,7 +100,7 @@ export_table_options = { "createdCell": function (td, cellData, rowData, row, col) { if (cellData !== '') { var images = ''; - if (rowData['thumb_level'] || rowData['art_level']) { + if (rowData['thumb_level'] || rowData['art_level'] || rowData['logo_level']) { images = ' + images'; } $(td).html(cellData + images); @@ -161,14 +161,14 @@ export_table_options = { if (cellData === 1 && rowData['exists']) { var tooltip_title = ''; var icon = ''; - if (rowData['thumb_level'] || rowData['art_level'] || rowData['individual_files']) { - tooltip_title = 'Zip Archive'; + if (rowData['thumb_level'] || rowData['art_level'] || rowData['logo_level'] || rowData['individual_files']) { + tooltip_title = 'ZIP Archive'; icon = 'fa-file-archive'; } else { tooltip_title = rowData['file_format'].toUpperCase() + ' File'; icon = 'fa-file-download'; } - var icon = (rowData['thumb_level'] || rowData['art_level'] || rowData['individual_files']) ? 'fa-file-archive' : 'fa-file-download'; + var icon = (rowData['thumb_level'] || rowData['art_level'] || rowData['logo_level'] || rowData['individual_files']) ? 'fa-file-archive' : 'fa-file-download'; $(td).html(''); } else if (cellData === 0) { var percent = Math.min(getPercent(rowData['exported_items'], rowData['total_items']), 99) diff --git a/data/interfaces/default/js/tables/history_table.js b/data/interfaces/default/js/tables/history_table.js index d209f90e..7f9d578f 100644 --- a/data/interfaces/default/js/tables/history_table.js +++ b/data/interfaces/default/js/tables/history_table.js @@ -263,13 +263,17 @@ history_table_options = { "targets": [12], "data": "watched_status", "createdCell": function (td, cellData, rowData, row, col) { + var circleValue = ""; if (cellData == 1) { - $(td).html(''); + circleValue = " circle-full"; + } else if (cellData == 0.75) { + circleValue = " circle-three-quarter"; } else if (cellData == 0.5) { - $(td).html(''); - } else { - $(td).html(''); + circleValue = " circle-half"; + } else if (cellData == 0.25) { + circleValue = " circle-quarter"; } + $(td).html('
    '); }, "searchable": false, "orderable": false, diff --git a/data/interfaces/default/library.html b/data/interfaces/default/library.html index b1fe8b6f..ba61153d 100644 --- a/data/interfaces/default/library.html +++ b/data/interfaces/default/library.html @@ -149,10 +149,10 @@ DOCUMENTATION :: END
    @@ -175,10 +175,10 @@ DOCUMENTATION :: END
    @@ -690,7 +690,8 @@ DOCUMENTATION :: END }, complete: function(xhr, status) { $("#library-recently-watched").html(xhr.responseText); - highlightWatchedScrollerButton(); + highlightScrollerButton("#recently-watched"); + paginateScroller("#recently-watched", ".paginate-watched"); } }); } @@ -706,7 +707,8 @@ DOCUMENTATION :: END }, complete: function(xhr, status) { $("#library-recently-added").html(xhr.responseText); - highlightAddedScrollerButton(); + highlightScrollerButton("#recently-added"); + paginateScroller("#recently-added", ".paginate-added"); } }); } @@ -716,83 +718,8 @@ DOCUMENTATION :: END recentlyAdded(); % endif - function highlightWatchedScrollerButton() { - var scroller = $("#recently-watched-row-scroller"); - var numElems = scroller.find("li").length; - scroller.width(numElems * 175); - if (scroller.width() > $("#library-recently-watched").width()) { - $("#recently-watched-page-right").removeClass("disabled"); - } else { - $("#recently-watched-page-right").addClass("disabled"); - } - } - - function highlightAddedScrollerButton() { - var scroller = $("#recently-added-row-scroller"); - var numElems = scroller.find("li").length; - scroller.width(numElems * 175); - if (scroller.width() > $("#library-recently-added").width()) { - $("#recently-added-page-right").removeClass("disabled"); - } else { - $("#recently-added-page-right").addClass("disabled"); - } - } - - $(window).resize(function() { - highlightWatchedScrollerButton(); - highlightAddedScrollerButton(); - }); - $('div.art-face').animate({ opacity: 0.2 }, { duration: 1000 }); - var leftTotalWatched = 0; - $(".paginate-watched").click(function (e) { - e.preventDefault(); - var scroller = $("#recently-watched-row-scroller"); - var containerWidth = $("#library-recently-watched").width(); - var scrollAmount = $(this).data("id") * parseInt(containerWidth / 175) * 175; - var leftMax = Math.min(-parseInt(scroller.width()) + Math.abs(scrollAmount), 0); - - leftTotalWatched = Math.max(Math.min(leftTotalWatched + scrollAmount, 0), leftMax); - scroller.animate({ left: leftTotalWatched }, 250); - - if (leftTotalWatched == 0) { - $("#recently-watched-page-left").addClass("disabled").blur(); - } else { - $("#recently-watched-page-left").removeClass("disabled"); - } - - if (leftTotalWatched == leftMax) { - $("#recently-watched-page-right").addClass("disabled").blur(); - } else { - $("#recently-watched-page-right").removeClass("disabled"); - } - }); - - var leftTotalAdded = 0; - $(".paginate-added").click(function (e) { - e.preventDefault(); - var scroller = $("#recently-added-row-scroller"); - var containerWidth = $("#library-recently-added").width(); - var scrollAmount = $(this).data("id") * parseInt(containerWidth / 175) * 175; - var leftMax = Math.min(-parseInt(scroller.width()) + Math.abs(scrollAmount), 0); - - leftTotalAdded = Math.max(Math.min(leftTotalAdded + scrollAmount, 0), leftMax); - scroller.animate({ left: leftTotalAdded }, 250); - - if (leftTotalAdded == 0) { - $("#recently-added-page-left").addClass("disabled").blur(); - } else { - $("#recently-added-page-left").removeClass("disabled"); - } - - if (leftTotalAdded == leftMax) { - $("#recently-added-page-right").addClass("disabled").blur(); - } else { - $("#recently-added-page-right").removeClass("disabled"); - } - }); - $(document).ready(function () { // Javascript to enable link to tab diff --git a/data/interfaces/default/library_recently_added.html b/data/interfaces/default/library_recently_added.html index fb53f9a5..4cf56d8e 100644 --- a/data/interfaces/default/library_recently_added.html +++ b/data/interfaces/default/library_recently_added.html @@ -36,7 +36,7 @@ DOCUMENTATION :: END %>
    -
    +
      % for item in data:
    • diff --git a/data/interfaces/default/login.html b/data/interfaces/default/login.html index 63b48deb..f8179fd1 100644 --- a/data/interfaces/default/login.html +++ b/data/interfaces/default/login.html @@ -11,6 +11,7 @@ + diff --git a/data/interfaces/default/mobile_device_config.html b/data/interfaces/default/mobile_device_config.html index 705ad568..78080603 100644 --- a/data/interfaces/default/mobile_device_config.html +++ b/data/interfaces/default/mobile_device_config.html @@ -3,7 +3,7 @@
    • diff --git a/data/interfaces/default/newsletter_config.html b/data/interfaces/default/newsletter_config.html index 10583707..f6ae70c3 100644 --- a/data/interfaces/default/newsletter_config.html +++ b/data/interfaces/default/newsletter_config.html @@ -13,7 +13,7 @@