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:
- Click the button below to continue to Coinbase. + Select a cryptocurrency. +
+ + ++ Or click the button below to continue to Coinbase.
Donate with Crypto @@ -335,6 +340,7 @@ ${next.modalIncludes()} + + @@ -356,6 +377,10 @@ break } + if (window.location.hash === '#concurrent-graph') { + current_tab = '#tabs-stream'; + } + $('#yaxis-' + yaxis).prop('checked', true); $('#yaxis-' + yaxis).closest('label').addClass('active'); $('#graph-days').val(current_day_range); @@ -540,6 +565,33 @@ } }); + $.ajax({ + url: "get_concurrent_streams_by_stream_type", + type: 'get', + data: { time_range: time_range, user_id: selected_user_id }, + dataType: "json", + success: function(data) { + var dateArray = []; + $.each(data.categories, function (i, day) { + dateArray.push(moment(day, 'YYYY-MM-DD').valueOf()); + // Highlight the weekend + if ((moment(day, 'YYYY-MM-DD').format('ddd') == 'Sat') || + (moment(day, 'YYYY-MM-DD').format('ddd') == 'Sun')) { + hc_plays_by_day_options.xAxis.plotBands.push({ + from: i-0.5, + to: i+0.5, + color: 'rgba(80,80,80,0.3)' + }); + } + }); + hc_concurrent_streams_by_stream_type_options.yAxis.min = 0; + hc_concurrent_streams_by_stream_type_options.xAxis.categories = dateArray; + hc_concurrent_streams_by_stream_type_options.series = getGraphVisibility(hc_concurrent_streams_by_stream_type_options.chart.renderTo, data.series); + hc_concurrent_streams_by_stream_type_options.colors = getGraphColors(data.series); + var hc_plays_by_stream_type = new Highcharts.Chart(hc_concurrent_streams_by_stream_type_options); + } + }); + $.ajax({ url: "get_plays_by_source_resolution", type: 'get', @@ -596,7 +648,7 @@ } }); - $('#nav-tabs-2').tab('show'); + $('#nav-tabs-stream').tab('show'); } function loadGraphsTab3(time_range, yaxis) { @@ -708,6 +760,7 @@ if (this.points.length > 1) { var total = 0; $.each(this.points, function(i, point) { + if (point.series.name === 'Total') return; s += 'day' + ((d > 1) ? 's' : '') + '
' - + 'hr' + ((h > 1) ? 's' : '') + '
' - + 'min' + ((m > 1) ? 's' : '') + '
'; - } else if (h > 0) { - text = 'hr' + ((h > 1) ? 's' : '') + '
' - + 'min' + ((m > 1) ? 's' : '') + '
'; - } else { - text = 'min' + ((m > 1) ? 's' : '') + '
'; + if (seconds > 0) { + return humanDuration(seconds * 1000).replaceAll(/(\d+) (\w+)/g, '$2
') } - - return text + return "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