diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml index 1185e5eb..62c3f86c 100644 --- a/.github/workflows/publish-docker.yml +++ b/.github/workflows/publish-docker.yml @@ -33,7 +33,6 @@ 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 @@ -59,7 +58,7 @@ jobs: 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@v3 @@ -69,8 +68,14 @@ jobs: 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@v5 + 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 diff --git a/.github/workflows/publish-installers.yml b/.github/workflows/publish-installers.yml index e3f7fe85..b4a66960 100644 --- a/.github/workflows/publish-installers.yml +++ b/.github/workflows/publish-installers.yml @@ -100,6 +100,24 @@ jobs: name: Tautulli-${{ matrix.os }}-installer 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 needs: build-installer diff --git a/.github/workflows/publish-snap.yml b/.github/workflows/publish-snap.yml index 539282e1..b3898a38 100644 --- a/.github/workflows/publish-snap.yml +++ b/.github/workflows/publish-snap.yml @@ -38,7 +38,7 @@ jobs: 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 }} @@ -50,7 +50,7 @@ jobs: 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 }} diff --git a/.github/workflows/submit-winget.yml b/.github/workflows/submit-winget.yml index aa1c4dec..5385c1c3 100644 --- a/.github/workflows/submit-winget.yml +++ b/.github/workflows/submit-winget.yml @@ -23,3 +23,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 e876fbb6..0cfe9f2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,109 @@ # Changelog +## 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: 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 8616df81..b3cf4736 100755 --- a/Tautulli.py +++ b/Tautulli.py @@ -34,6 +34,7 @@ import shutil import time import threading import tzlocal +import ctypes import plexpy from plexpy import common, config, database, helpers, logger, webstart @@ -69,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 @@ -110,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) diff --git a/data/interfaces/default/css/tautulli.css b/data/interfaces/default/css/tautulli.css index f8b15890..5cb5d6bf 100644 --- a/data/interfaces/default/css/tautulli.css +++ b/data/interfaces/default/css/tautulli.css @@ -1478,7 +1478,8 @@ a:hover .dashboard-stats-square { text-align: center; position: relative; z-index: 0; - overflow: hidden; + overflow: auto; + scrollbar-width: none; } .dashboard-recent-media { width: 100%; @@ -4324,6 +4325,10 @@ a:hover .overlay-refresh-image:hover { .stream-info tr:nth-child(even) td { background-color: rgba(255,255,255,0.010); } +.stream-info td:nth-child(3), +.stream-info th:nth-child(3) { + width: 25px; +} .number-input { margin: 0 !important; width: 55px !important; diff --git a/data/interfaces/default/current_activity_instance.html b/data/interfaces/default/current_activity_instance.html index fcc1b592..64d6f25e 100644 --- a/data/interfaces/default/current_activity_instance.html +++ b/data/interfaces/default/current_activity_instance.html @@ -74,6 +74,7 @@ DOCUMENTATION :: END parent_href = page('info', data['parent_rating_key']) grandparent_href = page('info', data['grandparent_rating_key']) user_href = page('user', data['user_id']) if data['user_id'] else '#' + library_href = page('library', data['section_id']) if data['section_id'] else '#' season = short_season(data['parent_title']) %>
+ Select the level to export logo image files.
Note: Only applies to ${logo_media_types}.
+
Warning: Exporting images may take a long time! Images will be saved to a folder alongside the data file.
@@ -231,6 +248,7 @@ DOCUMENTATION :: END $('#export_media_info_level').prop('disabled', true); $("#export_thumb_level").prop('disabled', true); $("#export_art_level").prop('disabled', true); + $("#export_logo_level").prop('disabled', true); export_custom_metadata_fields.disable(); export_custom_media_info_fields.disable(); } else { @@ -238,6 +256,7 @@ DOCUMENTATION :: END $('#export_media_info_level').prop('disabled', false); $("#export_thumb_level").prop('disabled', false); $("#export_art_level").prop('disabled', false); + $("#export_logo_level").prop('disabled', false); export_custom_metadata_fields.enable(); export_custom_media_info_fields.enable(); } @@ -252,6 +271,7 @@ DOCUMENTATION :: END var file_format = $('#export_file_format option:selected').val(); var thumb_level = $("#export_thumb_level option:selected").val(); var art_level = $("#export_art_level option:selected").val(); + var logo_level = $("#export_logo_level option:selected").val(); var custom_fields = [ $('#export_custom_metadata_fields').val(), $('#export_custom_media_info_fields').val() @@ -270,6 +290,7 @@ DOCUMENTATION :: END file_format: file_format, thumb_level: thumb_level, art_level: art_level, + logo_level: logo_level, custom_fields: custom_fields, export_type: export_type, individual_files: individual_files diff --git a/data/interfaces/default/graphs.html b/data/interfaces/default/graphs.html index 15a96a69..006d7ee9 100644 --- a/data/interfaces/default/graphs.html +++ b/data/interfaces/default/graphs.html @@ -301,6 +301,10 @@ return obj; }, {}); + if (!("Total" in chart_visibility)) { + chart_visibility["Total"] = false; + } + return data_series.map(function(s) { var obj = Object.assign({}, s); obj.visible = (chart_visibility[s.name] !== false); @@ -327,7 +331,8 @@ 'Direct Play': '#E5A00D', 'Direct Stream': '#FFFFFF', 'Transcode': '#F06464', - 'Max. Concurrent Streams': '#96C83C' + 'Max. Concurrent Streams': '#96C83C', + 'Total': '#96C83C' }; var series_colors = []; $.each(data_series, function(index, series) { diff --git a/data/interfaces/default/index.html b/data/interfaces/default/index.html index 6e4818c7..ca95be42 100644 --- a/data/interfaces/default/index.html +++ b/data/interfaces/default/index.html @@ -92,10 +92,10 @@Set the schedule for the newsletter. - Set the schedule for the newsletter using a custom crontab. Only standard cron values are valid. + + Set the schedule for the newsletter using a custom crontab. + Click here for a list of supported expressions. +
${sched_job.id} | Active | ${helpers.format_timedelta_Hms(sched_job.trigger.interval)} | -${helpers.format_timedelta_Hms(sched_job.next_run_time - now)} | +${helpers.format_timedelta_Hms(sched_job.next_run_time - datetime.datetime.now(sched_job.next_run_time.tzinfo))} | ${sched_job.next_run_time.astimezone(plexpy.SYS_TIMEZONE).strftime('%Y-%m-%d %H:%M:%S')} |
- | -- Stream Details - | +Source Details | ++ | + Stream Details + |
---|