diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..f535c9c7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.git +.github +.gitignore +*.md +!CHANGELOG*.md diff --git a/.github/workflows/publishdocker-branch.yml b/.github/workflows/publishdocker-branch.yml new file mode 100644 index 00000000..b8a6b336 --- /dev/null +++ b/.github/workflows/publishdocker-branch.yml @@ -0,0 +1,30 @@ +name: Publish Docker Branch +on: + push: + branches: [master, beta, nightly] +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@master + - name: Get Branch + run: echo ::set-env name=BRANCH::${GITHUB_REF#refs/heads/} + - name: Publish to Registry + uses: elgohr/Publish-Docker-Github-Action@master + env: + VERSION: ${{ github.sha }} + with: + name: tautulli/tautulli + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + dockerfile: Dockerfile + buildargs: VERSION, BRANCH + - name: Post Status to Discord + uses: sarisia/actions-status-discord@v1 + if: always() + with: + webhook: ${{ secrets.DISCORD_WEBHOOK }} + status: ${{ job.status }} + job: ${{ github.workflow }} + nofail: true diff --git a/.github/workflows/publishdocker-release.yml b/.github/workflows/publishdocker-release.yml new file mode 100644 index 00000000..96f5bb66 --- /dev/null +++ b/.github/workflows/publishdocker-release.yml @@ -0,0 +1,32 @@ +name: Publish Docker Release +on: + push: + tags: + - 'v*' +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@master + - name: Get Branch + run: echo ::set-env name=BRANCH::${GITHUB_REF/refs\/tags\//} + - name: Publish to Registry + uses: elgohr/Publish-Docker-Github-Action@master + env: + VERSION: ${{ github.sha }} + with: + name: tautulli/tautulli + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + dockerfile: Dockerfile + buildargs: VERSION, BRANCH + tags: ${{ env.BRANCH }} + - name: Post Status to Discord + uses: sarisia/actions-status-discord@v1 + if: always() + with: + webhook: ${{ secrets.DISCORD_WEBHOOK }} + status: ${{ job.status }} + job: ${{ github.workflow }} + nofail: true diff --git a/.github/workflows/publishrelease-beta.yml b/.github/workflows/publishrelease-beta.yml new file mode 100644 index 00000000..28183600 --- /dev/null +++ b/.github/workflows/publishrelease-beta.yml @@ -0,0 +1,29 @@ +name: Create Pre-Release +on: + push: + tags: + - 'v*-beta' +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@master + - name: Get Release Version + run: echo ::set-env name=RELEASE_VERSION::${GITHUB_REF/refs\/tags\//} + - name: Get Changelog + run: echo ::set-env name=CHANGELOG::"$( sed -n '/^## /{p; :loop n; p; /^## /q; b loop}' CHANGELOG.md | sed '$d' | sed '$d' | sed '$d' | sed ':a;N;$!ba;s/\n/%0A/g' )" + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ env.RELEASE_VERSION }} + release_name: Tautulli ${{ env.RELEASE_VERSION }} + body: | + ## Changelog + + ##${{ env.CHANGELOG }} + draft: false + prerelease: true diff --git a/.github/workflows/publishrelease-master.yml b/.github/workflows/publishrelease-master.yml new file mode 100644 index 00000000..e7795681 --- /dev/null +++ b/.github/workflows/publishrelease-master.yml @@ -0,0 +1,30 @@ +name: Create Release +on: + push: + tags: + - 'v*' + - '!v*-beta' +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@master + - name: Get Release Version + run: echo ::set-env name=RELEASE_VERSION::${GITHUB_REF/refs\/tags\//} + - name: Get Changelog + run: echo ::set-env name=CHANGELOG::"$( sed -n '/^## /{p; :loop n; p; /^## /q; b loop}' CHANGELOG.md | sed '$d' | sed '$d' | sed '$d' | sed ':a;N;$!ba;s/\n/%0A/g' )" + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ env.RELEASE_VERSION }} + release_name: Tautulli ${{ env.RELEASE_VERSION }} + body: | + ## Changelog + + ##${{ env.CHANGELOG }} + draft: false + prerelease: false diff --git a/API.md b/API.md index 20825dd7..09e471e0 100644 --- a/API.md +++ b/API.md @@ -395,7 +395,11 @@ Returns: "banner": "/library/metadata/1219/banner/1503306930", "bif_thumb": "/library/parts/274169/indexes/sd/1000", "bitrate": "10617", + "channel_call_sign": "", + "channel_identifier": "", "channel_stream": 0, + "channel_thumb": "", + "children_count": "", "collections": [], "container": "mkv", "content_rating": "TV-MA", @@ -427,13 +431,15 @@ Returns: "ip_address": "10.10.10.1", "ip_address_public": "64.123.23.111", "is_admin": 1, - "is_allow_sync": null, + "is_allow_sync": 1, "is_home_user": 1, "is_restricted": 0, "keep_history": 1, "labels": [], "last_viewed_at": "1462165717", "library_name": "TV Shows", + "live": 0, + "live_uuid": "", "local": "1", "location": "lan", "machine_id": "lmd93nkn12k29j2lnm", @@ -442,8 +448,8 @@ Returns: "optimized_version": 0, "optimized_version_profile": "", "optimized_version_title": "", - "originally_available_at": "2016-04-24", "original_title": "", + "originally_available_at": "2016-04-24", "parent_guid": "com.plexapp.agents.thetvdb://121361/6?lang=en", "parent_media_index": "6", "parent_rating_key": "153036", @@ -463,6 +469,7 @@ Returns: "rating_key": "153037", "relay": 0, "section_id": "2", + "secure": 1, "session_id": "helf15l3rxgw01xxe0jf3l3d", "session_key": "27", "shared_libraries": [ @@ -501,15 +508,21 @@ Returns: "stream_subtitle_location": "", "stream_video_bit_depth": "8", "stream_video_bitrate": "10233", + "stream_video_chroma_subsampling": "4:2:0", "stream_video_codec": "h264", "stream_video_codec_level": "41", + "stream_video_color_primaries": "", + "stream_video_color_range": "tv", + "stream_video_color_space": "bt709", + "stream_video_color_trc": "", "stream_video_decision": "direct play", + "stream_video_dynamic_range": "SDR", "stream_video_framerate": "24p", + "stream_video_full_resolution": "1080p", "stream_video_height": "1078", "stream_video_language": "", "stream_video_language_code": "", "stream_video_ref_frames": "4", - "stream_video_full_resolution": "1080p", "stream_video_resolution": "1080", "stream_video_scan_type": "progressive", "stream_video_width": "1920", @@ -559,9 +572,15 @@ Returns: "username": "LordCommanderSnow", "video_bit_depth": "8", "video_bitrate": "10233", + "video_chroma_subsampling": "4:2:0", "video_codec": "h264", "video_codec_level": "41", + "video_color_primaries": "", + "video_color_range": "tv", + "video_color_space": "bt709", + "video_color_trc": ", "video_decision": "direct play", + "video_dynamic_range": "SDR", "video_frame_rate": "23.976", "video_framerate": "24p", "video_full_resolution": "1080p", @@ -671,8 +690,9 @@ Optional parameters: grandparent_rating_key (int): 351 start_date (str): "YYYY-MM-DD" section_id (int): 2 - media_type (str): "movie", "episode", "track" + media_type (str): "movie", "episode", "track", "live" transcode_decision (str): "direct play", "copy", "transcode", + guid (str): Plex guid for an item, e.g. "com.plexapp.agents.thetvdb://121361/6/1" order_column (str): "date", "friendly_name", "ip_address", "platform", "player", "full_title", "started", "paused_counter", "stopped", "duration" order_dir (str): "desc" or "asc" @@ -697,10 +717,13 @@ Returns: "original_title": "", "group_count": 1, "group_ids": "1124", + "guid": "com.plexapp.agents.thetvdb://121361/6/1?lang=en", "id": 1124, "ip_address": "xxx.xxx.xxx.xxx", + "live": 0, "media_index": 17, "media_type": "episode", + "originally_available_at": "2016-04-24", "parent_media_index": 7, "parent_rating_key": 544, "parent_title": "", @@ -758,8 +781,10 @@ Returns: [{"content_rating": "TV-MA", "friendly_name": "", "grandparent_thumb": "/library/metadata/1219/thumb/1462175063", + "guid": "com.plexapp.agents.thetvdb://121361/6/1?lang=en", "labels": [], "last_play": 1462380698, + "live": 0, "media_type": "episode", "platform": "", "platform_type": "", @@ -860,15 +885,18 @@ Returns: "do_notify": "Checked", "do_notify_created": "Checked", "duration": 1578037, + "guid": "com.plexapp.agents.thetvdb://121361/6/1?lang=en", "id": 1128, "keep_history": "Checked", "labels": [], "last_accessed": 1462693216, "last_played": "Game of Thrones - The Red Woman", "library_art": "/:/resources/show-fanart.jpg", - "library_thumb": "", + "library_thumb": "/:/resources/show.png", + "live": 0, "media_index": 1, "media_type": "episode", + "originally_available_at": "2016-04-24", "parent_count": 240, "parent_media_index": 6, "parent_title": "", @@ -958,6 +986,7 @@ Returns: "rating_key": "1219", "section_id": 2, "section_type": "show", + "sort_title": "Game of Thrones", "thumb": "/library/metadata/1219/thumb/1436265995", "title": "Game of Thrones", "video_codec": "", @@ -1124,6 +1153,7 @@ Returns: "labels": [], "last_viewed_at": "1462165717", "library_name": "TV Shows", + "live": 0, "media_index": "1", "media_info": [ { @@ -1133,6 +1163,9 @@ Returns: "audio_codec": "ac3", "audio_profile": "", "bitrate": "10617", + "channel_call_sign": "", + "channel_identifier": "", + "channel_thumb": "", "container": "mkv", "height": "1078", "id": "257925", @@ -1151,6 +1184,10 @@ Returns: "video_bitrate": "10233", "video_codec": "h264", "video_codec_level": "41", + "video_color_primaries": "", + "video_color_range": "tv", + "video_color_space": "bt709", + "video_color_trc": "", "video_frame_rate": "23.976", "video_height": "1078", "video_language": "", @@ -1210,7 +1247,7 @@ Returns: "rating_image": "rottentomatoes://image.rating.ripe", "rating_key": "153037", "section_id": "2", - "sort_title": "Game of Thrones", + "sort_title": "Red Woman", "studio": "HBO", "summary": "Jon Snow is dead. Daenerys meets a strong man. Cersei sees her daughter again.", "tagline": "", @@ -1506,7 +1543,8 @@ Returns: "series": [{"name": "Movies", "data": [...]} {"name": "TV", "data": [...]}, - {"name": "Music", "data": [...]} + {"name": "Music", "data": [...]}, + {"name": "Live TV", "data": [...]} ] } ``` @@ -1532,7 +1570,8 @@ Returns: "series": [{"name": "Movies", "data": [...]} {"name": "TV", "data": [...]}, - {"name": "Music", "data": [...]} + {"name": "Music", "data": [...]}, + {"name": "Live TV", "data": [...]} ] } ``` @@ -1558,7 +1597,8 @@ Returns: "series": [{"name": "Movies", "data": [...]} {"name": "TV", "data": [...]}, - {"name": "Music", "data": [...]} + {"name": "Music", "data": [...]}, + {"name": "Live TV", "data": [...]} ] } ``` @@ -1662,7 +1702,8 @@ Returns: "series": [{"name": "Movies", "data": [...]} {"name": "TV", "data": [...]}, - {"name": "Music", "data": [...]} + {"name": "Music", "data": [...]}, + {"name": "Live TV", "data": [...]} ] } ``` @@ -1688,7 +1729,8 @@ Returns: "series": [{"name": "Movies", "data": [...]} {"name": "TV", "data": [...]}, - {"name": "Music", "data": [...]} + {"name": "Music", "data": [...]}, + {"name": "Live TV", "data": [...]} ] } ``` @@ -1714,7 +1756,8 @@ Returns: "series": [{"name": "Movies", "data": [...]} {"name": "TV", "data": [...]}, - {"name": "Music", "data": [...]} + {"name": "Music", "data": [...]}, + {"name": "Live TV", "data": [...]} ] } ``` @@ -1802,22 +1845,59 @@ Optional parameters: Returns: json: {"recently_added": - [{"added_at": "1461572396", + [{"actors": [ + "Kit Harington", + "Emilia Clarke", + "Isaac Hempstead-Wright", + "Maisie Williams", + "Liam Cunningham", + ], + "added_at": "1461572396", + "art": "/library/metadata/1219/art/1462175063", + "audience_rating": "8", + "audience_rating_image": "rottentomatoes://image.rating.upright", + "banner": "/library/metadata/1219/banner/1462175063", + "directors": [ + "Jeremy Podeswa" + ], + "duration": "2998290", + "full_title": "Game of Thrones - The Red Woman", + "genres": [ + "Adventure", + "Drama", + "Fantasy" + ], "grandparent_rating_key": "1219", "grandparent_thumb": "/library/metadata/1219/thumb/1462175063", "grandparent_title": "Game of Thrones", - "library_name": "", + "guid": "com.plexapp.agents.thetvdb://121361/6/1?lang=en", + "labels": [], + "last_viewed_at": "1462165717", + "library_name": "TV Shows", "media_index": "1", "media_type": "episode", "original_title": "", + "originally_available_at": "2016-04-24", "parent_media_index": "6", "parent_rating_key": "153036", "parent_thumb": "/library/metadata/153036/thumb/1462175062", "parent_title": "", + "rating": "7.8", + "rating_image": "rottentomatoes://image.rating.ripe", "rating_key": "153037", "section_id": "2", + "sort_title": "Red Woman", + "studio": "HBO", + "summary": "Jon Snow is dead. Daenerys meets a strong man. Cersei sees her daughter again.", + "tagline": "", "thumb": "/library/metadata/153037/thumb/1462175060", "title": "The Red Woman", + "user_rating": "9.0", + "updated_at": "1462175060", + "writers": [ + "David Benioff", + "D. B. Weiss" + ], "year": "2016" }, {...}, @@ -1999,6 +2079,7 @@ Returns: "stream_video_bitrate": 527, "stream_video_codec": "h264", "stream_video_decision": "transcode", + "stream_video_dynamic_range": "SDR", "stream_video_framerate": "24p", "stream_video_height": 306, "stream_video_resolution": "SD", @@ -2013,6 +2094,7 @@ Returns: "video_bitrate": 2500, "video_codec": "h264", "video_decision": "transcode", + "video_dynamic_range": "SDR", "video_framerate": "24p", "video_height": 816, "video_resolution": "1080", @@ -2166,12 +2248,15 @@ Returns: "recordsFiltered": 10, "data": [{"friendly_name": "Jon Snow", + "guid": "com.plexapp.agents.thetvdb://121361/6/1?lang=en", "id": 1121, "ip_address": "xxx.xxx.xxx.xxx", "last_played": "Game of Thrones - The Red Woman", "last_seen": 1462591869, + "live": 0, "media_index": 1, "media_type": "episode", + "originally_available_at": "2016-04-24", "parent_media_index": 6, "parent_title": "", "platform": "Chrome", @@ -2371,13 +2456,16 @@ Returns: "do_notify": "Checked", "duration": 2998290, "friendly_name": "Jon Snow", + "guid": "com.plexapp.agents.thetvdb://121361/6/1?lang=en", "id": 1121, "ip_address": "xxx.xxx.xxx.xxx", "keep_history": "Checked", "last_played": "Game of Thrones - The Red Woman", "last_seen": 1462591869, + "live": 0, "media_index": 1, "media_type": "episode", + "originally_available_at": "2016-04-24", "parent_media_index": 6, "parent_title": "", "platform": "Chrome", @@ -2516,10 +2604,10 @@ Optional parameters: width (str): 300 height (str): 450 opacity (str): 25 - background (str): 282828 + background (str): Hex color, e.g. 282828 blur (str): 3 img_format (str): png - fallback (str): "poster", "cover", "art" + fallback (str): "poster", "cover", "art", "poster-live", "art-live", "art-live-full" refresh (bool): True or False whether to refresh the image cache return_hash (bool): True or False to return the self-hosted image hash instead of the image diff --git a/CHANGELOG.md b/CHANGELOG.md index 41f2eeed..831d94e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,61 @@ # Changelog +## v2.2.0-beta (2020-02-27) + +* Important Note! + * All Live TV changes requires Plex Media Server 1.18.7 or higher. +* Monitoring: + * New: Added Live TV metadata and posters to the activity cards. + * Change: Show bandwidth in Gbps when greater than 1000 Mbps. +* History: + * New: Added history logging for Live TV sessions. + * New: Added a fake "Live TV" library to collect Live TV history. + * Note: This library will show up the first time that Live TV is played. + * New: Added the ability to filter history by Live TV. +* Graphs: + * New: Added Live TV series to the "Plays by Period" and "Play Totals" graphs. + * Change: Media type series on the graphs are only shown if the corresponding library type is present. +* Notifications: + * Fix: Race condition causing stream count to be incorrect for playback stop notifications. + * New: Added Live TV channel notification parameters. +* API: + * New: Added ability to filter history using a "live" media type and by guid for the get_history API command. +* Other: + * Change: Add crossorigin use-credentials attribute to manifest tags. (Thanks @pkoenig10) + * Change: Disable automatic updates for Docker containers. Updates are now handled by updating the Docker container. + * Note: If you are using an old Docker container created before v2.2.0, then you may need to completely remove and recreate the container to update for the first time. + * Note: Use the ":latest" Docker tag for the newest stable release, or the ":beta" or ":nightly" tags to access the beta or nightly branches. + + +## v2.1.44 (2020-02-05) + +* Monitoring: + * Fix: SDR source video being identified as HDR stream video. +* Notifications: + * Fix: Unable to select condition operator for video color parameters. +* UI: + * Fix: Capitalization for platforms on history tables. + + +## v2.1.43 (2020-02-03) + +* Monitoring: + * New: Added HDR indicator on activity card. + * New: Added dynamic range to history steam info modal. +* Notifications: + * Fix: Webhook notification body sent as incorrect data type when Content-Type header is overridden. + * Fix: Telegram notification character limit incorrect for unicode characters. + * New: Added color and dynamic range notification parameters. +* Newsletters: + * Fix: Episodes and Albums plural spelling on recently added newsletter section headers. +* UI: + * Fix: Windows and macOS platform capitalization. + * Fix: Season number 0 not shown for episodes on history tables. +* Other: + * Change: Mask email addresses in logs. + * Change: Update deprecated GitHub access token URL parameter to Authorization header. + + ## v2.1.42 (2020-01-04) * Other: diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..5efa135a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +FROM python:2.7.17-slim + +LABEL maintainer="TheMeanCanEHdian" + +ARG VERSION +ARG BRANCH + +ENV TAUTULLI_DOCKER=True +ENV TZ=UTC + +WORKDIR /app + +RUN \ +apt-get -q -y update --no-install-recommends && \ +apt-get install -q -y --no-install-recommends \ + curl && \ +rm -rf /var/lib/apt/lists/* && \ +pip install --no-cache-dir --upgrade pip && \ +pip install --no-cache-dir --upgrade \ + pycryptodomex \ + pyopenssl && \ +echo ${VERSION} > /app/version.txt && \ +echo ${BRANCH} > /app/branch.txt + +COPY . /app + +CMD [ "python", "Tautulli.py", "--datadir", "/config" ] + +VOLUME /config /plex_logs +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 diff --git a/README.md b/README.md index fd2f1dca..c74386ec 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,5 @@ # Tautulli -[![Discord](https://img.shields.io/badge/Discord-Tautulli-7289DA.svg?style=flat-square)](https://tautulli.com/discord) -[![Reddit](https://img.shields.io/badge/Reddit-Tautulli-FF5700.svg?style=flat-square)](https://www.reddit.com/r/Tautulli/) -[![Plex Forums](https://img.shields.io/badge/Plex%20Forums-Tautulli-E5A00D.svg?style=flat-square)](https://forums.plex.tv/t/tautulli-monitor-your-plex-media-server/225242) - A python based web application for monitoring, analytics and notifications for [Plex Media Server](https://plex.tv). This project is based on code from [Headphones](https://github.com/rembo10/headphones) and [PlexWatchWeb](https://github.com/ecleese/plexWatchWeb). @@ -31,7 +27,21 @@ This project is based on code from [Headphones](https://github.com/rembo10/headp ![Tautulli Homepage](https://tautulli.com/images/screenshots/activity-compressed.jpg?v=2) -## Installation and Support +## Installation & Support + +[![Python](https://img.shields.io/badge/python-v2.7.17-blue?style=flat-square)](https://python.org/downloads/release/python-2717/) +[![Docker Pulls](https://img.shields.io/docker/pulls/tautulli/tautulli?style=flat-square)](https://hub.docker.com/r/tautulli/tautulli) +[![Docker Stars](https://img.shields.io/docker/stars/tautulli/tautulli?style=flat-square)](https://hub.docker.com/r/tautulli/tautulli) + +| Status | Branch: `master` | Branch: `beta` | Branch: `nightly` | +| --- | --- | --- | --- | +| Release | [![Release@master](https://img.shields.io/github/v/release/Tautulli/Tautulli?style=flat-square)](https://github.com/Tautulli/Tautulli/releases/latest)
[![Release Date@master](https://img.shields.io/github/release-date/Tautulli/Tautulli?style=flat-square&color=blue)](https://github.com/Tautulli/Tautulli/releases/latest) | [![Release@beta](https://img.shields.io/github/v/release/Tautulli/Tautulli?include_prereleases&style=flat-square)](https://github.com/Tautulli/Tautulli/releases)
[![Commits@nightly](https://img.shields.io/github/commits-since/Tautulli/Tautulli/latest/beta?style=flat-square&color=blue)](https://github.com/Tautulli/Tautulli/commits/beta) | [![Last Commits@nightly](https://img.shields.io/github/last-commit/Tautulli/Tautulli/nightly?style=flat-square&color=blue)](https://github.com/Tautulli/Tautulli/commits/nightly)
[![Commits@nightly](https://img.shields.io/github/commits-since/Tautulli/Tautulli/latest/nightly?style=flat-square&color=blue)](https://github.com/Tautulli/Tautulli/commits/nightly) | +| Docker | [![Docker@master](https://img.shields.io/badge/tautulli-tautulli:latest-blue?style=flat-square)](https://hub.docker.com/r/tautulli/tautulli)
[![Docker Build@master](https://img.shields.io/github/workflow/status/Tautulli/Tautulli/Publish%20Docker%20Branch/master?style=flat-square)](https://github.com/Tautulli/Tautulli/actions?query=branch%3Amaster) | [![Docker@beta](https://img.shields.io/badge/tautulli-tautulli:beta-blue?style=flat-square)](https://hub.docker.com/r/tautulli/tautulli)
[![Docker Build@beta](https://img.shields.io/github/workflow/status/Tautulli/Tautulli/Publish%20Docker%20Branch/beta?style=flat-square)](https://github.com/Tautulli/Tautulli/actions?query=branch%3Abeta) | [![Docker@nightly](https://img.shields.io/badge/tautulli-tautulli:nightly-blue?style=flat-square)](https://hub.docker.com/r/tautulli/tautulli)
[![Docker Build@nightly](https://img.shields.io/github/workflow/status/Tautulli/Tautulli/Publish%20Docker%20Branch/nightly?style=flat-square)](https://github.com/Tautulli/Tautulli/actions?query=branch%3Anightly) | + +[![Wiki](https://img.shields.io/badge/github-wiki-black?style=flat-square)](https://github.com/Tautulli/Tautulli-Wiki/wiki) +[![Discord](https://img.shields.io/discord/183396325142822912?label=discord&style=flat-square&color=7289DA)](https://tautulli.com/discord) +[![Reddit](https://img.shields.io/reddit/subreddit-subscribers/tautulli?label=reddit&style=flat-square&color=FF5700)](https://www.reddit.com/r/Tautulli/) +[![Plex Forums](https://img.shields.io/badge/plex%20forums-discussion-E5A00D?style=flat-square)](https://forums.plex.tv/t/tautulli-monitor-your-plex-media-server/225242) * Read the [Installation Guides](https://github.com/Tautulli/Tautulli-Wiki/wiki/Installation) for instructions to install Tautulli. * The [Frequently Asked Questions](https://github.com/Tautulli/Tautulli-Wiki/wiki/Frequently-Asked-Questions) in the wiki can help you with common problems. @@ -39,10 +49,15 @@ This project is based on code from [Headphones](https://github.com/rembo10/headp ## Issues & Feature Requests +[![Issues](https://img.shields.io/badge/github-issues-red?style=flat-square)](https://github.com/Tautulli/Tautulli-Issues) +[![Feathub](https://img.shields.io/badge/feathub-requests-lightgrey?style=flat-square)](https://feathub.com/Tautulli/Tautulli) + * Please see the [Issues Repository](https://github.com/Tautulli/Tautulli-Issues). ## License +[![License](https://img.shields.io/github/license/Tautulli/Tautulli?style=flat-square)](https://github.com/Tautulli/Tautulli/blob/master/LICENSE) + This is free software under the GPL v3 open source license. Feel free to do with it what you wish, but any modification must be open sourced. A copy of the license is included. This software includes Highsoft software libraries which you may freely distribute for non-commercial use. Commerical users must licence this software, for more information visit https://shop.highsoft.com/faq/non-commercial#non-commercial-redistribution. \ No newline at end of file diff --git a/Tautulli.py b/Tautulli.py index 2517b8dc..d6e290a9 100755 --- a/Tautulli.py +++ b/Tautulli.py @@ -34,7 +34,7 @@ import time import tzlocal import plexpy -from plexpy import config, database, logger, webstart +from plexpy import config, database, helpers, logger, webstart # Register signals, such as CTRL + C @@ -115,7 +115,7 @@ def main(): plexpy.SYS_UTC_OFFSET = datetime.datetime.now(plexpy.SYS_TIMEZONE).strftime('%z') - if os.getenv('TAUTULLI_DOCKER', False) == 'True': + if helpers.bool_true(os.getenv('TAUTULLI_DOCKER', False)): plexpy.DOCKER = True if args.dev: diff --git a/data/interfaces/default/base.html b/data/interfaces/default/base.html index 38b7cc10..c32dcd08 100644 --- a/data/interfaces/default/base.html +++ b/data/interfaces/default/base.html @@ -28,7 +28,7 @@ - + @@ -43,23 +43,23 @@
% if _session['user_group'] == 'admin': - % if plexpy.CONFIG.CHECK_GITHUB and plexpy.UPDATE_AVAILABLE is None: + % if plexpy.CONFIG.CHECK_GITHUB and plexpy.UPDATE_AVAILABLE is not False: - % elif plexpy.CONFIG.CHECK_GITHUB and plexpy.UPDATE_AVAILABLE == 'release': - - % elif plexpy.CONFIG.CHECK_GITHUB and plexpy.UPDATE_AVAILABLE == 'commit': - % else: @@ -318,21 +318,23 @@ ${next.modalIncludes()} complete: function (xhr, status) { var result = $.parseJSON(xhr.responseText); var msg = ''; - if (result.update === null) { - msg = 'You are running an unknown version of Tautulli.
' + - 'Update or Dismiss'; - $('#updatebar').html(msg).fadeIn(); - } else if (result.update === true && result.release === true) { - msg = 'A new release (' + result.latest_release + ') of Tautulli is available!
' + - 'Update or Dismiss'; - $('#updatebar').html(msg).fadeIn(); - } else if (result.update === true && result.release === false) { - msg = 'A newer version of Tautulli is available!
' + - 'You are '+ result.commits_behind + ' commit' + (result.commits_behind > 1 ? 's' : '') + ' behind.
' + - 'Update or Dismiss'; - $('#updatebar').html(msg).fadeIn(); - } else if (result.update === false) { + if (result.update === false) { showMsg(' ' + result.message, false, true, 2000); + } else { + if (result.update === null) { + msg = 'You are running an unknown version of Tautulli.
'; + } else if (result.update === true && result.release === true) { + msg = 'A new release (' + result.latest_release + ') of Tautulli is available!
'; + } else if (result.update === true && result.release === false) { + msg = 'A newer version of Tautulli is available!
' + + 'You are '+ result.commits_behind + ' commit' + (result.commits_behind > 1 ? 's' : '') + ' behind.
'; + } + if (result.docker) { + msg += 'Update your Docker container or Dismiss'; + } else { + msg += 'Update or Dismiss'; + } + $('#updatebar').html(msg).fadeIn(); } if (_callback) { diff --git a/data/interfaces/default/css/dataTables.colVis.css b/data/interfaces/default/css/dataTables.colVis.css index 10d62649..94bcdd39 100644 --- a/data/interfaces/default/css/dataTables.colVis.css +++ b/data/interfaces/default/css/dataTables.colVis.css @@ -21,7 +21,7 @@ ul.ColVis_collection li { .ColVis_Button:hover, ul.ColVis_collection li:hover { - color: #F9AA03; + color: #E5A00D; } button.ColVis_Button { diff --git a/data/interfaces/default/css/tautulli-dataTables.css b/data/interfaces/default/css/tautulli-dataTables.css index f93c174b..248a7de8 100644 --- a/data/interfaces/default/css/tautulli-dataTables.css +++ b/data/interfaces/default/css/tautulli-dataTables.css @@ -101,7 +101,7 @@ table.display tr:hover a { text-decoration:none; } table.display td:hover a { - color: #F9AA03; + color: #E5A00D; } table.display thead tr:hover { background-color: #212121; diff --git a/data/interfaces/default/css/tautulli.css b/data/interfaces/default/css/tautulli.css index dad492e4..fad83b60 100644 --- a/data/interfaces/default/css/tautulli.css +++ b/data/interfaces/default/css/tautulli.css @@ -523,7 +523,7 @@ fieldset[disabled] .btn-bright.active { color: #eee; } .modal-body i { - color: #F9AA03; + color: #E5A00D; } .modal-body i.fa { color: #fff; @@ -534,7 +534,7 @@ fieldset[disabled] .btn-bright.active { } .modal-body strong, .modal-body strong i.fa { - color: #F9AA03; + color: #E5A00D; } .modal-footer { padding: 15px 20px; @@ -673,7 +673,7 @@ textarea.form-control:focus { color: #fff; } .form-control-feedback { - color: #F9AA03; + color: #E5A00D; margin: 5px 40px 5px 0; } .form-control[disabled], @@ -2177,7 +2177,7 @@ li.advanced-setting { font-size: 24px; color: #fff; padding-top: 27px; - padding-left: 110px; + padding-left: 105px; } .user-info-nav { margin-top: 15px; @@ -3134,6 +3134,37 @@ div.dataTables_info { -moz-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); } +.channel-thumbnail-popover { + z-index: 2000; + padding: 0; + border: 0; +} +.channel-thumbnail-popover.popover.left { + margin-left: -15px; +} +.channel-thumbnail-popover.popover.right { + margin-left: 15px; +} +.channel-thumbnail-popover .popover-content { + color: #000; + padding: 0; +} +.channel-thumbnail { + background-color: #868b8b; + background-position: center; + background-size: cover; + background-origin: content-box; + background-repeat: no-repeat; + height: 50px; + width: 50px; + padding: 3px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} +.channel-thumbnail-popover .arrow:after { + border-right-color: #868b8b !important; +} .edit-user-toggles, .edit-library-toggles { padding-right: 10px; @@ -3983,6 +4014,9 @@ a:hover .overlay-refresh-image:hover { .library-video { background-image: url(../images/libraries/video.svg); } +.library-live { + background-image: url(../images/libraries/live.svg); +} .stats-most_concurrent { background-image: url(../images/icons/most-concurrent-streams.svg); } @@ -4033,7 +4067,7 @@ a:hover .overlay-refresh-image:hover { table-layout: fixed; } .stream-info .heading { - color: #F9AA03; + color: #E5A00D; text-transform: uppercase; font-size: 15px; font-weight: bold !important; diff --git a/data/interfaces/default/current_activity_instance.html b/data/interfaces/default/current_activity_instance.html index 2abea8d7..4aec1378 100644 --- a/data/interfaces/default/current_activity_instance.html +++ b/data/interfaces/default/current_activity_instance.html @@ -62,8 +62,7 @@ DOCUMENTATION :: END % if session is not None: <% from collections import defaultdict - from six.moves.urllib.parse import quote - from plexpy import helpers + from plexpy.helpers import cast_to_int, page from plexpy.common import VIDEO_RESOLUTION_OVERRIDES, AUDIO_CODEC_OVERRIDES, EXTRA_TYPES import plexpy %> @@ -71,62 +70,67 @@ DOCUMENTATION :: END data = defaultdict(lambda: 'Unknown', **session) sk = data['session_key'] - href = 'info?rating_key={}'.format(data['rating_key']) if data['rating_key'] else '#' - parent_href = 'info?rating_key={}'.format(data['parent_rating_key']) if data['parent_rating_key'] else '#' - grandparent_href = 'info?rating_key={}'.format(data['grandparent_rating_key']) if data['grandparent_rating_key'] else '#' - user_href = 'user?user_id={}'.format(data['user_id']) if data['user_id'] else '#' + href = page('info', data['rating_key']) + 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 '#' %>
+ data-rating_key="${data['rating_key']}" data-parent_rating_key="${data['parent_rating_key']}" data-grandparent_rating_key="${data['grandparent_rating_key']}" + data-guid="${data['guid']}">
<% - if data['live'] == 1: - background_url = 'images/art-live.png' + if data['live']: + background_url = page('pms_image_proxy', data['art'], data['rating_key'], 500, 280, 40, '282828', 3, fallback='art-live', refresh=True) elif data['channel_stream'] == 0: - background_url = 'pms_image_proxy?img=' + data['art'] + '&width=500&height=280&opacity=40&background=282828&blur=3&fallback=art&refresh=true' + background_url = page('pms_image_proxy', data['art'], data['rating_key'], 500, 280, 40, '282828', 3, fallback='art', refresh=True) else: - if (data['art'] and data['art'].startswith('http')) or (data['thumb'] and data['thumb'].startswith('http')): - background_url = data['art'] - else: - background_url = 'pms_image_proxy?img=' + quote(data['art'] or data['thumb']) + '&width=500&height=280&fallback=art&refresh=true&clip=true' + background_url = page('pms_image_proxy', data['art'] or data['thumb'], data['rating_key'], 500, 280, 40, '282828', 3, fallback='art', refresh=True, clip=True) %>
@@ -160,7 +164,7 @@ DOCUMENTATION :: END
% if data['media_type'] != 'photo' and data['quality_profile'] != 'Unknown': <% - br = helpers.cast_to_int(data['stream_bitrate']) or '' + br = cast_to_int(data['stream_bitrate']) or '' if br: if br > 1000: br = '(' + str(round(br / 1000.0, 1)) + ' Mbps)' @@ -326,8 +330,10 @@ DOCUMENTATION :: END
% if data['media_type'] != 'photo' and data['bandwidth'] != 'Unknown': <% - bw = helpers.cast_to_int(data['bandwidth']) - if bw > 1000: + bw = cast_to_int(data['bandwidth']) + if bw > 1000000: + bw = str(round(bw / 1000000.0, 1)) + ' Gbps' + elif bw > 1000: bw = str(round(bw / 1000.0, 1)) + ' Mbps' else: bw = str(bw) + ' kbps' @@ -346,8 +352,8 @@ DOCUMENTATION :: END
% if data['media_type'] != 'photo':
- % if data['live'] == 1: -
Live + % if data['live']: +
${data['channel_call_sign']} ${data['channel_identifier']} % elif data['view_offset']: ETA: @@ -376,8 +382,8 @@ DOCUMENTATION :: END
- % if data['live'] == 1: -
Live
+ % if data['live']: +
Live
% else:
${data['transcode_progress']}%
${data['progress_percent']}%
@@ -400,7 +406,16 @@ DOCUMENTATION :: END % endif