Compare commits
24 Commits
v2.6.0-bet
...
v2.6.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a8adad7dbb | ||
![]() |
4cfa5ac10b | ||
![]() |
55090ddeaa | ||
![]() |
14346b0e69 | ||
![]() |
ac24acf9ce | ||
![]() |
4cde62fde9 | ||
![]() |
7489bc8d98 | ||
![]() |
cde9287d85 | ||
![]() |
558023e18e | ||
![]() |
8157ee7811 | ||
![]() |
d746d2913f | ||
![]() |
0136fc6436 | ||
![]() |
7ce280cb92 | ||
![]() |
0209fa87aa | ||
![]() |
62cc2f769f | ||
![]() |
a49d44c880 | ||
![]() |
dab288380a | ||
![]() |
2ac5c35065 | ||
![]() |
ec9e2fe0f0 | ||
![]() |
ecbe79b5b9 | ||
![]() |
c4ac03738b | ||
![]() |
352dbd9bc8 | ||
![]() |
393b395df0 | ||
![]() |
1a96da04a1 |
25
.github/workflows/publish-docker.yml
vendored
@@ -49,13 +49,21 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
|
||||
- name: Docker Login
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
if: success()
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v1
|
||||
if: success()
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.GHCR_TOKEN }}
|
||||
|
||||
- name: Docker Build and Push
|
||||
uses: docker/build-push-action@v2
|
||||
if: success()
|
||||
@@ -65,19 +73,16 @@ jobs:
|
||||
push: true
|
||||
platforms: ${{ steps.prepare.outputs.docker_platforms }}
|
||||
build-args: |
|
||||
TAG=${{ steps.prepare.outputs.tag }},
|
||||
BRANCH=${{ steps.prepare.outputs.branch }},
|
||||
COMMIT=${{ steps.prepare.outputs.commit }},
|
||||
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 }}
|
||||
tags: |
|
||||
${{ steps.prepare.outputs.docker_image }}:${{ steps.prepare.outputs.tag }}
|
||||
ghcr.io/${{ steps.prepare.outputs.docker_image }}:${{ steps.prepare.outputs.tag }}
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||
|
||||
- name: Clear
|
||||
if: always()
|
||||
run: |
|
||||
rm -f ${HOME}/.docker/config.json
|
||||
|
||||
- name: Post Status to Discord
|
||||
uses: sarisia/actions-status-discord@v1
|
||||
if: always()
|
||||
|
@@ -1,24 +1,30 @@
|
||||
# Changelog
|
||||
|
||||
## v2.6.0-beta (2020-10-16)
|
||||
## v2.6.0 (2020-10-31)
|
||||
|
||||
* Exporter:
|
||||
* New: New exporter feature that allows you to export the metadata and images for any library, collection, playlist, or media item to csv, json, xml, or m3u8. Refer to the Exporter Guide in the wiki for more details.
|
||||
* UI:
|
||||
* Fix: Margin on the homepage activity and statistic/library cards. (Thanks @dotsam)
|
||||
* Fix: Movie ratings not showing on the info page for the new Plex Movie agent.
|
||||
* New: Added ability to browse collections and playlists from the library and user pages.
|
||||
* Change: Updated platform brand logos and colours.
|
||||
* API:
|
||||
* New: Added export_metadata, download_export, and delete_export API commands.
|
||||
* New: Added get_collections_table, and get_playlists_table API commands.
|
||||
* New: Added min_version parameter to the register_device API command.
|
||||
* New: Added include_activity parameter to the get_history API command.
|
||||
* New: Added sync_id parameter to the get_metadata API command.
|
||||
* New: Added delete_synced_item API command.
|
||||
* New: Added a stat_id and stats_start parameters to the get_home_stats API command.
|
||||
* New: Allow deleting a mobile device using the registration device_id for the delete_mobile_device API command.
|
||||
* Change: Return Plex server info and Tautulli info from the register_device command.
|
||||
* Other:
|
||||
* New: The Docker container is now also built for the arm32v6 architecture.
|
||||
* New: The Docker container is also published to the GitHub Container Registry at ghcr.io/tautulli/tautulli.
|
||||
* Change: Tautulli is now using a forked version of plexapi 3.6.0. This is to support the exporter feature while still maintaining Python 2 compatibility.
|
||||
* Change: Updated systemd script to remove process forking. (Thanks @MichaIng)
|
||||
* Change: Cache GitHub update check on startup.
|
||||
|
||||
|
||||
## v2.5.6 (2020-10-02)
|
||||
|
@@ -11,13 +11,16 @@ ENV TZ=UTC
|
||||
WORKDIR /app
|
||||
|
||||
RUN \
|
||||
groupadd -g 1000 tautulli && \
|
||||
useradd -u 1000 -g 1000 tautulli && \
|
||||
echo ${BRANCH} > /app/branch.txt && \
|
||||
echo ${COMMIT} > /app/version.txt
|
||||
|
||||
COPY . /app
|
||||
|
||||
CMD [ "python", "Tautulli.py", "--datadir", "/config" ]
|
||||
ENTRYPOINT [ "./start.sh" ]
|
||||
|
||||
VOLUME /config
|
||||
EXPOSE 8181
|
||||
HEALTHCHECK --start-period=90s CMD curl -ILfSs http://localhost:8181/status > /dev/null || curl -ILfkSs https://localhost:8181/status > /dev/null || exit 1
|
||||
HEALTHCHECK --start-period=90s CMD curl -ILfSs http://localhost:8181/status > /dev/null || curl -ILfkSs https://localhost:8181/status > /dev/null || exit 1
|
||||
|
@@ -24,21 +24,21 @@
|
||||
${next.headIncludes()}
|
||||
|
||||
<!-- Favicons -->
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.0.5">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.0.5">
|
||||
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.0.5">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.6.0">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.6.0">
|
||||
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.6.0">
|
||||
|
||||
<!-- ICONS -->
|
||||
<!-- Android -->
|
||||
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.0.5" crossorigin="use-credentials">
|
||||
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.6.0" crossorigin="use-credentials">
|
||||
<meta name="theme-color" content="#282a2d">
|
||||
<!-- Apple -->
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.5">
|
||||
<link rel="mask-icon" href="${http_root}images/favicon/safari-pinned-tab.svg?v=2.0.5" color="#282a2d">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.6.0">
|
||||
<link rel="mask-icon" href="${http_root}images/favicon/safari-pinned-tab.svg?v=2.6.0" color="#282a2d">
|
||||
<meta name="apple-mobile-web-app-title" content="Tautulli">
|
||||
<!-- Microsoft -->
|
||||
<meta name="application-name" content="Tautulli">
|
||||
<meta name="msapplication-config" content="${http_root}images/favicon/browserconfig.xml?v=2.0.5">
|
||||
<meta name="msapplication-config" content="${http_root}images/favicon/browserconfig.xml?v=2.6.0">
|
||||
</head>
|
||||
|
||||
<body class="content">
|
||||
|
@@ -750,7 +750,9 @@ a .users-poster-face:hover {
|
||||
position: relative;
|
||||
}
|
||||
#dashboard-checking-activity,
|
||||
#dashboard-no-activity {
|
||||
#dashboard-no-activity,
|
||||
#dashboard-checking-recently-added,
|
||||
#dashboard-no-recently-added {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.dashboard-activity-instance {
|
||||
@@ -1446,9 +1448,6 @@ a:hover .dashboard-stats-square {
|
||||
-moz-box-shadow: inset 0 0 0 2px #e9a049;
|
||||
box-shadow: inset 0 0 0 2px #e9a049;
|
||||
}
|
||||
#dashboard-no-recently-added {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.dashboard-recent-media-row {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
@@ -3850,19 +3849,19 @@ a:hover .overlay-refresh-image:hover {
|
||||
background-position: center !important;
|
||||
}
|
||||
.platform-android {
|
||||
background-color: #a4ca39;
|
||||
background-color: #3ddc84;
|
||||
background-image: url(../images/platforms/android.svg);
|
||||
}
|
||||
.platform-atv {
|
||||
background-color: #858487;
|
||||
background-color: #a2aaad;
|
||||
background-image: url(../images/platforms/atv.svg);
|
||||
}
|
||||
.platform-chrome {
|
||||
background-color: #ed5e50;
|
||||
background-color: #db4437;
|
||||
background-image: url(../images/platforms/chrome.svg);
|
||||
}
|
||||
.platform-chromecast {
|
||||
background-color: #10a4e8;
|
||||
background-color: #4285f4;
|
||||
background-image: url(../images/platforms/chromecast.svg);
|
||||
}
|
||||
.platform-default {
|
||||
@@ -3870,11 +3869,11 @@ a:hover .overlay-refresh-image:hover {
|
||||
background-image: url(../images/platforms/default.svg);
|
||||
}
|
||||
.platform-dlna {
|
||||
background-color: #0cb14b;
|
||||
background-color: #4ba32f;
|
||||
background-image: url(../images/platforms/dlna.svg);
|
||||
}
|
||||
.platform-firefox {
|
||||
background-color: #e67817;
|
||||
background-color: #ff7139;
|
||||
background-image: url(../images/platforms/firefox.svg);
|
||||
}
|
||||
.platform-gtv {
|
||||
@@ -3882,27 +3881,27 @@ a:hover .overlay-refresh-image:hover {
|
||||
background-image: url(../images/platforms/gtv.svg);
|
||||
}
|
||||
.platform-ie {
|
||||
background-color: #00599e;
|
||||
background-color: #18bcef;
|
||||
background-image: url(../images/platforms/ie.svg);
|
||||
}
|
||||
.platform-ios {
|
||||
background-color: #858487;
|
||||
background-color: #a2aaad;
|
||||
background-image: url(../images/platforms/ios.svg);
|
||||
}
|
||||
.platform-kodi {
|
||||
background-color: #31afe1;
|
||||
background-color: #30aada;
|
||||
background-image: url(../images/platforms/kodi.svg);
|
||||
}
|
||||
.platform-lg {
|
||||
background-color: #a50034;
|
||||
background-color: #990033;
|
||||
background-image: url(../images/platforms/lg.svg);
|
||||
}
|
||||
.platform-linux {
|
||||
background-color: #1793d0;
|
||||
background-color: #0099cc;
|
||||
background-image: url(../images/platforms/linux.svg);
|
||||
}
|
||||
.platform-macos {
|
||||
background-color: #858487;
|
||||
background-color: #a2aaad;
|
||||
background-image: url(../images/platforms/macos.svg);
|
||||
}
|
||||
.platform-msedge {
|
||||
@@ -3910,11 +3909,11 @@ a:hover .overlay-refresh-image:hover {
|
||||
background-image: url(../images/platforms/msedge.svg);
|
||||
}
|
||||
.platform-opera {
|
||||
background-color: #ff1b2d;
|
||||
background-color: #fa1e4e;
|
||||
background-image: url(../images/platforms/opera.svg);
|
||||
}
|
||||
.platform-playstation {
|
||||
background-color: #034da2;
|
||||
background-color: #003087;
|
||||
background-image: url(../images/platforms/playstation.svg);
|
||||
}
|
||||
.platform-plex {
|
||||
@@ -3926,11 +3925,11 @@ a:hover .overlay-refresh-image:hover {
|
||||
background-image: url(../images/platforms/plexamp.svg);
|
||||
}
|
||||
.platform-roku {
|
||||
background-color: #6d3c97;
|
||||
background-color: #673293;
|
||||
background-image: url(../images/platforms/roku.svg);
|
||||
}
|
||||
.platform-safari {
|
||||
background-color: #00a9ec;
|
||||
background-color: #00d3f9;
|
||||
background-image: url(../images/platforms/safari.svg);
|
||||
}
|
||||
.platform-samsung {
|
||||
@@ -3950,7 +3949,7 @@ a:hover .overlay-refresh-image:hover {
|
||||
background-image: url(../images/platforms/wiiu.svg);
|
||||
}
|
||||
.platform-windows {
|
||||
background-color: #2fc0f5;
|
||||
background-color: #0078d7;
|
||||
background-image: url(../images/platforms/windows.svg);
|
||||
}
|
||||
.platform-wp {
|
||||
@@ -3966,55 +3965,55 @@ a:hover .overlay-refresh-image:hover {
|
||||
background-image: url(../images/platforms/xbox.svg);
|
||||
}
|
||||
.platform-android-rgba {
|
||||
background-color: rgba(164, 202, 57, 0.40);
|
||||
background-color: rgba(61, 220, 132, 0.40);
|
||||
}
|
||||
.platform-atv-rgba {
|
||||
background-color: rgba(133, 132, 135, 0.40);
|
||||
background-color: rgba(162, 170, 173, 0.40);
|
||||
}
|
||||
.platform-chrome-rgba {
|
||||
background-color: rgba(237, 94, 80, 0.40);
|
||||
background-color: rgba(219, 68, 55, 0.40);
|
||||
}
|
||||
.platform-chromecast-rgba {
|
||||
background-color: rgba(16, 164, 232, 0.40);
|
||||
background-color: rgba(66, 133, 244, 0.40);
|
||||
}
|
||||
.platform-default-rgba {
|
||||
background-color: rgba(229, 160, 13, 0.40);
|
||||
}
|
||||
.platform-dlna-rgba {
|
||||
background-color: rgba(12, 177, 75, 0.40);
|
||||
background-color: rgba(75, 163, 47, 0.40);
|
||||
}
|
||||
.platform-firefox-rgba {
|
||||
background-color: rgba(230, 120, 23, 0.40);
|
||||
background-color: rgba(255, 113, 57, 0.40);
|
||||
}
|
||||
.platform-gtv-rgba {
|
||||
background-color: rgba(0, 139, 207, 0.40);
|
||||
}
|
||||
.platform-ie-rgba {
|
||||
background-color: rgba(0, 89, 158, 0.40);
|
||||
background-color: rgba(24, 188, 239, 0.40);
|
||||
}
|
||||
.platform-ios-rgba {
|
||||
background-color: rgba(133, 132, 135, 0.40);
|
||||
background-color: rgba(162, 170, 173, 0.40);
|
||||
}
|
||||
.platform-kodi-rgba {
|
||||
background-color: rgba(49, 175, 225, 0.40);
|
||||
background-color: rgba(48, 170, 218, 0.40);
|
||||
}
|
||||
.platform-lg-rgba {
|
||||
background-color: rgba(165, 0, 52, 0.40);
|
||||
background-color: rgba(153, 0, 51, 0.40);
|
||||
}
|
||||
.platform-linux-rgba {
|
||||
background-color: rgba(23, 147, 208, 0.40);
|
||||
background-color: rgba(0, 153, 204, 0.40);
|
||||
}
|
||||
.platform-macos-rgba {
|
||||
background-color: rgba(133, 132, 135, 0.40);
|
||||
background-color: rgba(162, 170, 173, 0.40);
|
||||
}
|
||||
.platform-msedge-rgba {
|
||||
background-color: rgba(0, 120, 215, 0.40);
|
||||
}
|
||||
.platform-opera-rgba {
|
||||
background-color: rgba(255, 27, 45, 0.40);
|
||||
background-color: rgba(250, 30, 78, 0.40);
|
||||
}
|
||||
.platform-playstation-rgba {
|
||||
background-color: rgba(3, 77, 162, 0.40);
|
||||
background-color: rgba(0, 48, 135, 0.40);
|
||||
}
|
||||
.platform-plex-rgba {
|
||||
background-color: rgba(229, 160, 13, 0.40);
|
||||
@@ -4023,10 +4022,10 @@ a:hover .overlay-refresh-image:hover {
|
||||
background-color: rgba(229, 160, 13, 0.40);
|
||||
}
|
||||
.platform-roku-rgba {
|
||||
background-color: rgba(109, 60, 151, 0.40);
|
||||
background-color: rgba(103, 50, 147, 0.40);
|
||||
}
|
||||
.platform-safari-rgba {
|
||||
background-color: rgba(0, 169, 236, 0.40);
|
||||
background-color: rgba(0, 211, 249, 0.40);
|
||||
}
|
||||
.platform-samsung-rgba {
|
||||
background-color: rgba(3, 78, 162, 0.40);
|
||||
@@ -4041,7 +4040,7 @@ a:hover .overlay-refresh-image:hover {
|
||||
background-color: rgba(3, 169, 244, 0.40);
|
||||
}
|
||||
.platform-windows-rgba {
|
||||
background-color: rgba(47, 192, 245, 0.40);
|
||||
background-color: rgba(0, 120, 215, 0.40);
|
||||
}
|
||||
.platform-wp-rgba {
|
||||
background-color: rgba(104, 33, 122, 0.40);
|
||||
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 4.5 KiB |
@@ -2,7 +2,7 @@
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="mstile-150x150.png?v=2.0.5"/>
|
||||
<square150x150logo src="mstile-150x150.png?v=2.6.0"/>
|
||||
<TileColor>#282a2d</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
|
Before Width: | Height: | Size: 553 B After Width: | Height: | Size: 997 B |
Before Width: | Height: | Size: 971 B After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -6,12 +6,12 @@
|
||||
"scope": "../../",
|
||||
"icons": [
|
||||
{
|
||||
"src": "android-chrome-192x192.png?v=2.0.5",
|
||||
"src": "android-chrome-192x192.png?v=2.6.0",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "android-chrome-256x256.png?v=2.0.5",
|
||||
"src": "android-chrome-256x256.png?v=2.6.0",
|
||||
"sizes": "256x256",
|
||||
"type": "image/png"
|
||||
}
|
||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 10 KiB |
@@ -1 +1,32 @@
|
||||
<svg version="1" xmlns="http://www.w3.org/2000/svg" width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000" preserveAspectRatio="xMidYMid meet"><g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)" fill="#000000" stroke="none"><path d="M5695 6555 c-135 -34 -244 -94 -342 -189 -40 -39 -73 -76 -73 -83 0 -7 -4 -13 -10 -13 -14 0 -87 -156 -106 -225 -22 -83 -26 -234 -8 -320 17 -79 86 -230 133 -288 l30 -39 -48 -71 c-39 -57 -159 -228 -251 -357 -69 -97 -398 -564 -416 -590 -13 -19 -60 -87 -105 -150 -45 -63 -107 -151 -138 -195 -30 -44 -59 -84 -63 -90 -7 -9 -251 -354 -346 -490 -92 -131 -173 -245 -175 -245 -1 0 -34 9 -72 21 -130 38 -325 31 -454 -18 -168 -63 -313 -196 -385 -354 -39 -87 -65 -183 -68 -256 0 -24 -3 -43 -4 -43 -2 0 -43 46 -91 102 -49 57 -100 117 -115 133 -14 17 -128 149 -253 295 -125 146 -251 292 -279 324 -56 65 -77 89 -108 126 -58 68 -152 178 -172 200 -12 14 -50 57 -83 96 l-61 71 27 44 c58 93 91 217 92 342 2 161 -38 294 -125 412 -133 181 -316 279 -542 292 -470 27 -833 -434 -699 -887 74 -251 275 -437 530 -490 132 -28 334 -6 421 45 l42 24 173 -197 c96 -108 186 -210 200 -227 15 -16 163 -187 330 -380 458 -529 491 -567 526 -605 18 -19 31 -35 30 -36 -6 -5 -265 -161 -277 -167 -8 -4 -34 -20 -58 -35 -194 -124 -634 -382 -651 -382 -12 0 -46 20 -75 44 -60 49 -180 112 -242 127 -21 5 -48 12 -59 15 -11 4 -65 9 -121 11 -81 4 -117 1 -182 -15 -261 -66 -462 -270 -528 -537 -10 -40 -11 -217 -2 -258 5 -23 11 -51 14 -61 29 -145 147 -312 284 -403 123 -82 224 -114 370 -118 83 -3 124 2 240 29 36 9 133 57 187 94 60 41 111 91 153 152 14 19 28 37 32 40 19 15 71 140 89 217 17 73 20 107 16 198 -4 61 -7 121 -9 134 -3 28 -46 0 482 321 179 108 379 228 444 265 104 59 120 65 133 52 13 -13 12 -22 -10 -78 -49 -123 -58 -165 -62 -262 -7 -149 25 -286 89 -383 47 -72 91 -128 125 -158 19 -17 39 -36 45 -42 27 -25 136 -94 150 -94 8 0 17 -4 20 -9 3 -5 16 -11 28 -14 13 -3 50 -12 83 -21 74 -19 278 -15 345 7 198 65 358 196 435 358 16 34 20 36 49 28 17 -4 49 -10 71 -14 22 -3 99 -16 170 -30 72 -13 144 -26 160 -29 28 -5 101 -18 170 -31 17 -3 80 -14 140 -25 61 -11 124 -22 140 -25 17 -4 49 -9 72 -12 40 -5 42 -7 48 -47 14 -98 29 -147 73 -235 36 -75 61 -110 121 -171 154 -154 280 -210 480 -213 134 -2 180 5 273 40 212 83 371 262 427 481 24 93 25 255 2 342 -64 241 -245 428 -481 501 -62 18 -97 23 -200 22 -107 0 -136 -4 -205 -26 -44 -15 -109 -43 -145 -64 -83 -48 -208 -171 -250 -245 -17 -32 -35 -60 -38 -61 -4 -2 -46 4 -93 13 -48 10 -104 20 -125 23 -22 3 -46 8 -54 11 -8 3 -33 7 -55 10 -38 5 -58 9 -122 21 -16 3 -53 10 -83 15 -30 6 -66 12 -79 15 -13 2 -103 19 -200 36 -169 30 -207 42 -196 60 10 16 -28 155 -62 224 -19 39 -54 96 -78 127 l-45 58 40 52 c96 125 143 266 143 433 1 164 -27 263 -108 391 -19 30 -35 57 -35 61 0 3 31 49 69 102 57 81 450 638 625 889 28 40 62 88 76 107 14 18 194 274 400 568 291 414 379 534 393 531 10 -2 27 -6 37 -9 78 -25 240 -29 338 -9 433 87 677 573 489 974 -93 200 -255 332 -478 389 -87 22 -227 25 -304 6z"/></g></svg>
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="350.000000pt" height="350.000000pt" viewBox="0 0 350.000000 350.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.11, written by Peter Selinger 2001-2013
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,350.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M1566 3489 c-433 -46 -867 -274 -1141 -601 -404 -481 -526 -1100
|
||||
-334 -1688 91 -278 283 -569 498 -756 676 -589 1646 -589 2322 0 215 187 407
|
||||
478 498 756 142 436 113 895 -84 1305 -320 666 -1027 1061 -1759 984z m1147
|
||||
-604 c87 -36 146 -118 154 -214 10 -111 -39 -203 -137 -254 -49 -26 -63 -28
|
||||
-131 -25 l-76 3 -109 -154 c-60 -85 -190 -269 -290 -409 l-181 -255 26 -46
|
||||
c22 -38 26 -59 26 -121 0 -63 -5 -84 -29 -132 -27 -54 -28 -59 -13 -76 22 -24
|
||||
47 -86 47 -117 0 -14 6 -28 13 -30 6 -3 91 -16 187 -30 157 -23 175 -24 183
|
||||
-10 38 68 115 118 199 130 103 15 220 -51 268 -151 26 -52 29 -154 6 -207 -19
|
||||
-48 -82 -114 -129 -138 -151 -77 -346 22 -373 189 -7 46 15 39 -222 74 -142
|
||||
20 -155 21 -163 6 -65 -116 -225 -163 -347 -102 -116 58 -167 187 -126 323 8
|
||||
29 13 55 11 57 -3 3 -65 -33 -138 -79 -74 -46 -162 -100 -196 -120 l-62 -38 6
|
||||
-47 c11 -100 -46 -207 -136 -254 -43 -23 -66 -28 -121 -28 -77 0 -124 16 -175
|
||||
62 -48 41 -76 99 -82 167 -7 72 9 129 50 183 85 112 256 132 372 44 l31 -24
|
||||
174 109 c96 60 180 111 185 113 6 2 -2 16 -16 32 -35 39 -412 468 -414 471 0
|
||||
1 -21 -5 -45 -13 -57 -20 -142 -14 -196 14 -162 84 -197 288 -71 419 102 108
|
||||
291 101 386 -14 62 -75 78 -185 40 -273 l-21 -49 23 -28 c13 -16 102 -118 198
|
||||
-227 l175 -198 20 61 c26 78 64 125 124 155 63 31 117 39 177 26 49 -11 51
|
||||
-11 72 17 21 26 533 749 548 773 4 6 -4 28 -17 48 -88 133 -44 307 94 376 61
|
||||
31 163 36 221 11z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 1.8 KiB |
@@ -1,8 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||
<title>android</title>
|
||||
<path fill="#fff" d="M31.944 21.318c5.556 0 11.113 0 16.67 0 0.042 0 0.084-0 0.126 0.001 0.548 0.012 0.554 0.012 0.554 0.555 0.002 2.526 0.001 5.052 0.001 7.577 0 5.789 0.003 11.577-0.002 17.365-0.001 1.197-0.344 2.274-1.205 3.155-0.759 0.777-1.671 1.191-2.753 1.22-0.757 0.019-1.515 0.011-2.273 0.016-0.772 0.005-0.774 0.006-0.774 0.751-0.001 2.505-0.032 5.010 0.013 7.514 0.024 1.305-0.386 2.363-1.302 3.29-1.214 1.23-3.457 1.485-4.769 0.396-1.051-0.873-1.725-1.978-1.715-3.423 0.019-2.547 0.010-5.093 0.003-7.64-0.003-1.010 0.144-0.869-0.858-0.876-1.158-0.008-2.315-0.005-3.473-0.001-0.829 0.003-0.76-0.103-0.76 0.794-0.002 2.505-0.027 5.010 0.010 7.514 0.019 1.278-0.377 2.325-1.281 3.235-1.199 1.208-3.371 1.494-4.716 0.437-1.067-0.838-1.779-1.932-1.77-3.386 0.017-2.61 0.005-5.219 0.005-7.829 0-0.147-0.008-0.295 0-0.442 0.013-0.24-0.092-0.339-0.334-0.335-0.736 0.012-1.473 0.002-2.209 0.022-0.575 0.015-1.129-0.058-1.673-0.251-1.682-0.597-2.691-2.017-2.737-3.858-0.063-2.566-0.031-5.135-0.035-7.703-0.007-5.304-0.010-10.608-0.016-15.912-0.001-0.568-0.017-1.136-0.018-1.704-0-0.464 0.006-0.472 0.494-0.479 0.989-0.013 1.978-0.023 2.968-0.023 4.609-0.002 9.219-0.001 13.829-0.001-0.001 0.006-0.001 0.014-0.001 0.021z"></path>
|
||||
<path fill="#fff" d="M31.944 19.89c-5.535 0-11.071 0.002-16.606-0.002-0.717-0-0.772 0.153-0.687-0.747 0.189-2.003 0.58-3.948 1.437-5.784 1.041-2.228 2.47-4.152 4.433-5.648 0.864-0.658 1.646-1.43 2.624-1.932 0.216-0.111 0.25-0.23 0.129-0.443-0.363-0.64-0.715-1.286-1.059-1.937-0.441-0.835-0.877-1.674-1.302-2.518-0.247-0.491-0.206-0.765 0.103-0.941 0.342-0.194 0.625-0.077 0.892 0.415 0.721 1.329 1.429 2.664 2.142 3.997 0.069 0.13 0.141 0.258 0.215 0.386 0.226 0.39 0.228 0.394 0.671 0.218 2.478-0.987 5.051-1.43 7.715-1.338 2.143 0.074 4.214 0.501 6.214 1.273 0.118 0.045 0.241 0.081 0.35 0.142 0.186 0.102 0.303 0.067 0.405-0.126 0.534-1.023 1.075-2.043 1.617-3.062 0.297-0.557 0.592-1.115 0.908-1.66 0.189-0.325 0.514-0.408 0.809-0.253 0.292 0.153 0.366 0.43 0.175 0.817-0.39 0.79-0.791 1.575-1.204 2.353-0.383 0.725-0.789 1.438-1.18 2.159-0.19 0.351-0.181 0.348 0.158 0.573 1.666 1.102 3.266 2.297 4.577 3.814 1.895 2.192 3.115 4.723 3.574 7.598 0.119 0.746 0.175 1.503 0.266 2.254 0.038 0.311-0.097 0.421-0.393 0.394-0.146-0.014-0.295-0.002-0.442-0.002-5.514 0-11.028 0-16.543 0zM25.561 12.038c-0.063-1.117-0.623-1.553-1.433-1.566-0.833-0.014-1.419 0.462-1.455 1.603-0.025 0.776 0.66 1.407 1.463 1.409 0.79 0.001 1.421-0.64 1.424-1.445zM39.872 13.483c0.788-0.007 1.497-0.676 1.439-1.441-0.076-0.997-0.486-1.549-1.506-1.576-0.841-0.022-1.403 0.67-1.386 1.605 0.016 0.816 0.635 1.418 1.453 1.411z"></path>
|
||||
<path fill="#fff" d="M50.587 32.655c0-2.715-0.003-5.429 0.001-8.143 0.003-1.77 0.853-2.959 2.453-3.698 0.717-0.331 1.433-0.52 2.172-0.287 0.794 0.251 1.537 0.649 2.123 1.273 0.519 0.552 0.839 1.207 0.944 1.957 0.052 0.374 0.082 0.754 0.083 1.131 0.005 5.282-0.005 10.564 0.010 15.846 0.004 1.249-0.402 2.288-1.278 3.179-1.245 1.267-3.35 1.546-4.76 0.479-1.076-0.815-1.719-1.943-1.745-3.342-0.019-1.010-0.013-2.020-0.014-3.030-0.002-1.789-0.001-3.578-0.001-5.366 0.004-0 0.008-0 0.012-0z"></path>
|
||||
<path fill="#fff" d="M13.369 32.464c0 2.335-0.001 4.669 0.001 7.004 0 0.63 0.047 1.263 0.002 1.889-0.072 1.003-0.541 1.811-1.23 2.554-0.931 1.004-2.059 1.18-3.323 1.058-1.55-0.15-3.156-2.028-3.181-3.665-0.004-0.231-0.015-0.462-0.014-0.694 0.003-5.406 0.007-10.812 0.011-16.218 0.001-1.655 0.863-2.749 2.268-3.501 0.683-0.366 1.397-0.602 2.158-0.402 1.622 0.427 3.305 1.697 3.292 3.834-0.016 2.713-0.004 5.427-0.004 8.141 0.007-0 0.013-0 0.020 0z"></path>
|
||||
<path fill="#fff" d="M46.73 40.88c-0.003 0-0.007 0-0.010 0-1.475 0-2.67-1.195-2.67-2.67s1.195-2.67 2.67-2.67c1.475 0 2.67 1.195 2.67 2.67v0c0 0 0 0 0 0 0 1.471-1.19 2.664-2.659 2.67h-0.001zM17.27 40.88c-1.475 0-2.67-1.195-2.67-2.67s1.195-2.67 2.67-2.67c1.475 0 2.67 1.195 2.67 2.67v0c0 0.003 0 0.007 0 0.010 0 1.469-1.191 2.66-2.66 2.66-0.003 0-0.007 0-0.011 0h0.001zM47.68 24.83l5.32-9.23c0.095-0.159 0.151-0.351 0.151-0.557 0-0.405-0.219-0.76-0.546-0.951l-0.005-0.003c-0.16-0.095-0.354-0.152-0.56-0.152-0.407 0-0.764 0.22-0.957 0.547l-0.003 0.005-5.38 9.34c-4.027-1.851-8.738-2.93-13.7-2.93s-9.673 1.079-13.909 3.016l0.209-0.086-5.39-9.34c-0.204-0.28-0.531-0.46-0.9-0.46-0.613 0-1.11 0.497-1.11 1.11 0 0.167 0.037 0.325 0.103 0.467l-0.003-0.007 5.33 9.23c-9.153 5.047-15.453 14.286-16.323 25.059l-0.007 0.111h64c-0.875-10.883-7.171-20.121-16.158-25.088l-0.162-0.082z"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 1.0 KiB |
@@ -24,17 +24,13 @@
|
||||
</div>
|
||||
<div id="currentActivity">
|
||||
% if PLEX_SERVER_UP:
|
||||
<div class="text-muted" id="dashboard-checking-activity"><i class="fa fa-refresh fa-spin"></i> Checking for activity...</div>
|
||||
<div class="text-muted" id="dashboard-checking-activity"><i class="fa fa-refresh fa-spin"></i> Checking for activity...</div>
|
||||
% elif config['pms_is_cloud']:
|
||||
<div id="dashboard-no-activity" class="text-muted">Plex Cloud server is sleeping.</div>
|
||||
% elif not config['first_run_complete']:
|
||||
<div id="dashboard-no-activity" class="text-muted">The Tautulli setup wizard has not been completed. Please click <a href="welcome">here</a> to go to the setup wizard.</div>
|
||||
% else:
|
||||
<div id="dashboard-no-activity" class="text-muted">There was an error communicating with your Plex Server.
|
||||
% if _session['user_group'] == 'admin':
|
||||
Check the <a href="logs">logs</a> and verify your server connection in the <a href="settings#tab_tabs-plex_media_server">settings</a>.
|
||||
% endif
|
||||
</div>
|
||||
<div class="text-muted" id="dashboard-checking-activity"><i class="fa fa-refresh fa-spin"></i> Tautulli is connecting to the Plex server...</div>
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
@@ -65,7 +61,7 @@
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div id="home-stats" class="home-platforms">
|
||||
<div class="text-muted"><i class="fa fa-refresh fa-spin"></i> Loading stats...</div>
|
||||
<div class="text-muted"><i class="fa fa-refresh fa-spin"></i> Loading stats...</div>
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
@@ -84,7 +80,7 @@
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div id="library-stats" class="library-platforms">
|
||||
<div class="text-muted"><i class="fa fa-refresh fa-spin"></i> Loading stats...</div>
|
||||
<div class="text-muted"><i class="fa fa-refresh fa-spin"></i> Loading stats...</div>
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
@@ -132,17 +128,12 @@
|
||||
<div class="col-md-12">
|
||||
<div id="recentlyAdded" style="margin-right: -15px;">
|
||||
% if PLEX_SERVER_UP:
|
||||
<div class="text-muted"><i class="fa fa-refresh fa-spin"></i> Looking for new items...</div>
|
||||
<div id="dashboard-checking-recently-added" class="text-muted"><i class="fa fa-refresh fa-spin"></i> Looking for new items...</div>
|
||||
% elif config['pms_is_cloud']:
|
||||
<div class="text-muted">Plex Cloud server is sleeping.</div>
|
||||
% else:
|
||||
<div class="text-muted">There was an error communicating with your Plex Server.
|
||||
% if _session['user_group'] == 'admin':
|
||||
Check the <a href="logs">logs</a> and verify your server connection in the <a href="settings#tab_tabs-plex_media_server">settings</a>.
|
||||
% endif
|
||||
</div>
|
||||
<div id="dashboard-no-recently-added" class="text-muted"><i class="fa fa-refresh fa-spin"></i> Tautulli is connecting to your Plex server...</div>
|
||||
% endif
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -229,7 +220,6 @@
|
||||
</%def>
|
||||
|
||||
<%def name="javascriptIncludes()">
|
||||
<% from plexpy import PLEX_SERVER_UP %>
|
||||
<script src="${http_root}js/jquery.scrollbar.min.js"></script>
|
||||
<script src="${http_root}js/jquery.mousewheel.min.js"></script>
|
||||
<script>
|
||||
@@ -259,8 +249,33 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
% if _session['user_group'] == 'admin':
|
||||
var msg_settings = ' Check the <a href="logs">logs</a> and verify your server connection in the <a href="settings#tab_tabs-plex_media_server">settings</a>.';
|
||||
% else:
|
||||
var msg_settings = '';
|
||||
% endif
|
||||
|
||||
var error_msg = 'There was an error communicating with your Plex Server.' + msg_settings;
|
||||
|
||||
var server_status;
|
||||
server_status = setInterval(function() {
|
||||
$.getJSON('server_status', function (data) {
|
||||
if (data.connected === true) {
|
||||
clearInterval(server_status);
|
||||
$('#currentActivity').html('<div id="dashboard-checking-activity" class="text-muted"><i class="fa fa-refresh fa-spin"></i> Checking for activity...</div>');
|
||||
$('#recentlyAdded').html('<div id="dashboard-checking-recently-added" class="text-muted"><i class="fa fa-refresh fa-spin"></i> Looking for new items...</div>');
|
||||
activityConnected();
|
||||
recentlyAddedConnected();
|
||||
} else if (data.connected === false) {
|
||||
clearInterval(server_status);
|
||||
$('#currentActivity').html('<div id="dashboard-no-activity" class="text-muted">' + error_msg + '</div>');
|
||||
$('#recentlyAdded').html('<div id="dashboard-no-recently-added" class="text-muted">' + error_msg + '</div>');
|
||||
}
|
||||
});
|
||||
}, 1000);
|
||||
</script>
|
||||
% if 'current_activity' in config['home_sections'] and PLEX_SERVER_UP:
|
||||
% if 'current_activity' in config['home_sections']:
|
||||
<script>
|
||||
var defaultHandler = {
|
||||
get: function(target, name) {
|
||||
@@ -271,7 +286,7 @@
|
||||
var create_instances = [];
|
||||
var activity_ready = true;
|
||||
|
||||
$('#currentActivityHeader-bandwidth-tooltip').tooltip({ container: 'body', placement: 'right', delay: 50 });
|
||||
$('#currentActivityHeader-bandwidth-tooltip').tooltip({ container: 'body', placement: 'right', delay: 50 });
|
||||
|
||||
function getCurrentActivity() {
|
||||
activity_ready = false;
|
||||
@@ -297,13 +312,8 @@
|
||||
}
|
||||
|
||||
if (!(current_activity)) {
|
||||
% if _session['user_group'] == 'admin':
|
||||
var msg_settings = ' Check the <a href="logs">logs</a> and verify your server connection in the <a href="settings#tab_tabs-plex_media_server">settings</a>.';
|
||||
% else:
|
||||
var msg_settings = '';
|
||||
% endif
|
||||
$('#currentActivityHeader').hide();
|
||||
$('#currentActivity').html('<div id="dashboard-no-activity" class="text-muted">There was an error communicating with your Plex Server.' + msg_settings + '</div>');
|
||||
$('#currentActivity').html('<div id="dashboard-no-activity" class="text-muted">' + error_msg + '</div>');
|
||||
return
|
||||
}
|
||||
|
||||
@@ -548,7 +558,7 @@
|
||||
}
|
||||
|
||||
// Update the progress bars, percent - 3 because of 3px padding-right
|
||||
$('#buffer-bar-' + key).width(parseInt(s.transcode_progress) - 3 + '%').html(s.transcode_progress + '%')
|
||||
$('#buffer-bar-' + key).css({width: parseInt(s.transcode_progress) - 3 + '%'}).html(s.transcode_progress + '%')
|
||||
.attr('data-original-title', 'Transcoder Progress ' + s.transcode_progress + '%');
|
||||
if (s.live !== 1) {
|
||||
var progress_bar = $('#progress-bar-' + key);
|
||||
@@ -625,34 +635,36 @@
|
||||
});
|
||||
}
|
||||
|
||||
getCurrentActivity();
|
||||
setInterval(function () {
|
||||
if (!(create_instances.length) && activity_ready) {
|
||||
getCurrentActivity();
|
||||
}
|
||||
}, ${config['home_refresh_interval'] * 1000});
|
||||
function activityConnected() {
|
||||
getCurrentActivity();
|
||||
setInterval(function () {
|
||||
if (!(create_instances.length) && activity_ready) {
|
||||
getCurrentActivity();
|
||||
}
|
||||
}, ${config['home_refresh_interval'] * 1000});
|
||||
|
||||
setInterval(function(){
|
||||
$('.progress_time_offset').each(function () {
|
||||
if ($(this).data('state') === 'playing' && $(this).data('view_offset') >= 0) {
|
||||
var view_offset = parseInt($(this).data('view_offset'));
|
||||
var stream_duration = parseInt($(this).data('stream_duration'));
|
||||
var timestamp = millisecondsToMinutes(Math.min(view_offset, stream_duration), false);
|
||||
$(this).html(timestamp).data('view_offset', Math.min(view_offset + 1000, stream_duration))
|
||||
}
|
||||
});
|
||||
$('.progress-bar').each(function () {
|
||||
if ($(this).data('state') === 'playing' && $(this).data('view_offset') >= 0) {
|
||||
var view_offset = parseInt($(this).data('view_offset'));
|
||||
var stream_duration = parseInt($(this).data('stream_duration'));
|
||||
var progress_percent = Math.floor(view_offset / stream_duration * 100);
|
||||
progress_percent = (progress_percent >= 0) ? Math.min(progress_percent, 100) : 100;
|
||||
$(this).width(progress_percent - 3 + '%').html(progress_percent + '%')
|
||||
.attr('data-original-title', 'Stream Progress ' + progress_percent + '%')
|
||||
.data('view_offset', Math.min(view_offset + 1000, stream_duration));
|
||||
}
|
||||
});
|
||||
}, 1000);
|
||||
setInterval(function(){
|
||||
$('.progress_time_offset').each(function () {
|
||||
if ($(this).data('state') === 'playing' && $(this).data('view_offset') >= 0) {
|
||||
var view_offset = parseInt($(this).data('view_offset'));
|
||||
var stream_duration = parseInt($(this).data('stream_duration'));
|
||||
var timestamp = millisecondsToMinutes(Math.min(view_offset, stream_duration), false);
|
||||
$(this).html(timestamp).data('view_offset', Math.min(view_offset + 1000, stream_duration))
|
||||
}
|
||||
});
|
||||
$('.progress-bar').each(function () {
|
||||
if ($(this).data('state') === 'playing' && $(this).data('view_offset') >= 0) {
|
||||
var view_offset = parseInt($(this).data('view_offset'));
|
||||
var stream_duration = parseInt($(this).data('stream_duration'));
|
||||
var progress_percent = Math.floor(view_offset / stream_duration * 100);
|
||||
progress_percent = (progress_percent >= 0) ? Math.min(progress_percent, 100) : 100;
|
||||
$(this).css({width: progress_percent - 3 + '%'}).html(progress_percent + '%')
|
||||
.attr('data-original-title', 'Stream Progress ' + progress_percent + '%')
|
||||
.data('view_offset', Math.min(view_offset + 1000, stream_duration));
|
||||
}
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
$('#currentActivity').on('click', '.external_ip-modal', function () {
|
||||
$.get('get_ip_address_details', {
|
||||
@@ -876,7 +888,7 @@
|
||||
getLibraryStats();
|
||||
</script>
|
||||
% endif
|
||||
% if 'recently_added' in config['home_sections'] and PLEX_SERVER_UP:
|
||||
% if 'recently_added' in config['home_sections']:
|
||||
<script>
|
||||
function recentlyAdded(recently_added_count, recently_added_type) {
|
||||
showMsg("Loading recently added items...", true, false, 0);
|
||||
@@ -904,7 +916,9 @@
|
||||
$('#recently-added-toggle-' + recently_added_type).closest('label').addClass('active');
|
||||
$('#recently-added-count').val(recently_added_count);
|
||||
|
||||
recentlyAdded(recently_added_count, recently_added_type);
|
||||
function recentlyAddedConnected() {
|
||||
recentlyAdded(recently_added_count, recently_added_type);
|
||||
}
|
||||
|
||||
function highlightAddedScrollerButton() {
|
||||
var scroller = $("#recently-added-row-scroller");
|
||||
|
@@ -303,16 +303,17 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
<div class="summary-content">
|
||||
<div class="summary-content-details-wrapper">
|
||||
% if data['rating']:
|
||||
% if data['rating_image']:
|
||||
% if data['rating_image'].startswith('imdb://'):
|
||||
<div class="critic-rating hidden-xs hidden-sm" title="${data['rating']}">
|
||||
<span class="rating-image rating-imdb"><strong>${data['rating']}</strong></span>
|
||||
<% rating = data['rating'] or data['audience_rating'] %>
|
||||
% if rating:
|
||||
% if data['audience_rating_image']:
|
||||
% if data['audience_rating_image'].startswith('imdb://'):
|
||||
<div class="critic-rating hidden-xs hidden-sm" title="${rating}">
|
||||
<span class="rating-image rating-imdb"><strong>${rating}</strong></span>
|
||||
</div>
|
||||
% endif
|
||||
% if data['rating_image'].startswith('themoviedb://'):
|
||||
<div class="critic-rating hidden-xs hidden-sm" title="${data['rating']}">
|
||||
<span class="rating-image rating-themoviedb"><strong>${get_percent(data['rating'], 10)}%</strong></span>
|
||||
% if data['audience_rating_image'].startswith('themoviedb://'):
|
||||
<div class="critic-rating hidden-xs hidden-sm" title="${rating}">
|
||||
<span class="rating-image rating-themoviedb"><strong>${get_percent(rating, 10)}%</strong></span>
|
||||
</div>
|
||||
% endif
|
||||
% if data['audience_rating_image'].startswith('rottentomatoes://'):
|
||||
@@ -326,8 +327,8 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
% endif
|
||||
% else:
|
||||
<div class="critic-rating hidden-xs hidden-sm" title="${data['rating']}">
|
||||
<i class="star-icon fa fa-star"></i> <strong>${get_percent(data['rating'], 10)}%</strong>
|
||||
<div class="critic-rating hidden-xs hidden-sm" title="${rating}">
|
||||
<i class="star-icon fa fa-star"></i> <strong>${get_percent(rating, 10)}%</strong>
|
||||
</div>
|
||||
% endif
|
||||
% endif
|
||||
|
@@ -18,21 +18,21 @@
|
||||
<link href="${http_root}css/font-awesome.v4-shims.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Favicons -->
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.0.5">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.0.5">
|
||||
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.0.5">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.6.0">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.6.0">
|
||||
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.6.0">
|
||||
|
||||
<!-- ICONS -->
|
||||
<!-- Android -->
|
||||
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.0.5" crossorigin="use-credentials">
|
||||
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.6.0" crossorigin="use-credentials">
|
||||
<meta name="theme-color" content="#282a2d">
|
||||
<!-- Apple -->
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.5">
|
||||
<link rel="mask-icon" href="${http_root}images/favicon/safari-pinned-tab.svg?v=2.0.5" color="#282a2d">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.6.0">
|
||||
<link rel="mask-icon" href="${http_root}images/favicon/safari-pinned-tab.svg?v=2.6.0" color="#282a2d">
|
||||
<meta name="apple-mobile-web-app-title" content="Tautulli">
|
||||
<!-- Microsoft -->
|
||||
<meta name="application-name" content="Tautulli">
|
||||
<meta name="msapplication-config" content="${http_root}images/favicon/browserconfig.xml?v=2.0.5">
|
||||
<meta name="msapplication-config" content="${http_root}images/favicon/browserconfig.xml?v=2.6.0">
|
||||
</head>
|
||||
|
||||
<body style="margin: 0; overflow: auto;">
|
||||
|
@@ -8,9 +8,9 @@
|
||||
<meta charset="utf-8">
|
||||
<title>Tautulli - ${title} | ${server_name}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.0.5">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.0.5">
|
||||
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.0.5">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.6.0">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.6.0">
|
||||
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.6.0">
|
||||
<link href="${http_root}css/tautulli.css${cache_param}" rel="stylesheet">
|
||||
<style>
|
||||
* {
|
||||
|
@@ -21,21 +21,21 @@
|
||||
<link href="${http_root}css/font-awesome.v4-shims.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Favicons -->
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.0.5">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.0.5">
|
||||
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.0.5">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.6.0">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.6.0">
|
||||
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.6.0">
|
||||
|
||||
<!-- ICONS -->
|
||||
<!-- Android -->
|
||||
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.0.5" crossorigin="use-credentials">
|
||||
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.6.0" crossorigin="use-credentials">
|
||||
<meta name="theme-color" content="#282a2d">
|
||||
<!-- Apple -->
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.5">
|
||||
<link rel="mask-icon" href="${http_root}images/favicon/safari-pinned-tab.svg?v=2.0.5" color="#282a2d">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.6.0">
|
||||
<link rel="mask-icon" href="${http_root}images/favicon/safari-pinned-tab.svg?v=2.6.0" color="#282a2d">
|
||||
<meta name="apple-mobile-web-app-title" content="Tautulli">
|
||||
<!-- Microsoft -->
|
||||
<meta name="application-name" content="Tautulli">
|
||||
<meta name="msapplication-config" content="${http_root}images/favicon/browserconfig.xml?v=2.0.5">
|
||||
<meta name="msapplication-config" content="${http_root}images/favicon/browserconfig.xml?v=2.6.0">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@@ -521,7 +521,7 @@
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
.apple-link a {
|
||||
a[x-apple-data-detectors] {
|
||||
color: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
@@ -1087,4 +1087,4 @@
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
% endif
|
||||
% endif
|
||||
|
@@ -521,7 +521,7 @@
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
.apple-link a {
|
||||
a[x-apple-data-detectors] {
|
||||
color: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
@@ -1087,4 +1087,4 @@
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
% endif
|
||||
% endif
|
||||
|
@@ -231,7 +231,7 @@ class Album(Audio):
|
||||
self.guid = data.attrib.get('guid')
|
||||
self.leafCount = utils.cast(int, data.attrib.get('leafCount'))
|
||||
self.loudnessAnalysisVersion = utils.cast(int, data.attrib.get('loudnessAnalysisVersion'))
|
||||
self.key = self.key.replace('/children', '') # fixes bug #50
|
||||
self.key = self.key.replace('/children', '') # FIX_BUG_50
|
||||
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt'), '%Y-%m-%d')
|
||||
self.parentGuid = data.attrib.get('parentGuid')
|
||||
self.parentKey = data.attrib.get('parentKey')
|
||||
|
@@ -334,7 +334,7 @@ class PlexPartialObject(PlexObject):
|
||||
search result for a movie often only contain a portion of the attributes a full
|
||||
object (main url) for that movie contain.
|
||||
"""
|
||||
return not self.key or self.key == self._initpath
|
||||
return not self.key or (self._details_key or self.key) == self._initpath
|
||||
|
||||
def isPartialObject(self):
|
||||
""" Returns True if this is not a full object. """
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from plexapi import X_PLEX_CONTAINER_SIZE, log, utils, media
|
||||
from plexapi.base import PlexObject
|
||||
from plexapi.base import PlexObject, PlexPartialObject
|
||||
from plexapi.compat import quote, quote_plus, unquote, urlencode
|
||||
from plexapi.exceptions import BadRequest, NotFound
|
||||
from plexapi.media import MediaTag
|
||||
@@ -765,10 +765,17 @@ class MovieSection(LibrarySection):
|
||||
METADATA_TYPE = 'movie'
|
||||
CONTENT_TYPE = 'video'
|
||||
|
||||
def all(self, **kwargs):
|
||||
""" Returns a list of all items from this library section.
|
||||
See description of :func:`plexapi.library.LibrarySection.search()` for details about filtering / sorting.
|
||||
"""
|
||||
return self.search(libtype='movie', **kwargs)
|
||||
|
||||
def collection(self, **kwargs):
|
||||
""" Returns a list of collections from this library section. """
|
||||
key = '/library/sections/%s/collections' % self.key
|
||||
return self.fetchItems(key, **kwargs)
|
||||
""" Returns a list of collections from this library section.
|
||||
See description of :func:`plexapi.library.LibrarySection.search()` for details about filtering / sorting.
|
||||
"""
|
||||
return self.search(libtype='collection', **kwargs)
|
||||
|
||||
def playlist(self, **kwargs):
|
||||
""" Returns a list of playlists from this library section. """
|
||||
@@ -851,10 +858,17 @@ class ShowSection(LibrarySection):
|
||||
"""
|
||||
return self.search(sort='addedAt:desc', libtype=libtype, maxresults=maxresults)
|
||||
|
||||
def all(self, libtype='show', **kwargs):
|
||||
""" Returns a list of all items from this library section.
|
||||
See description of :func:`plexapi.library.LibrarySection.search()` for details about filtering / sorting.
|
||||
"""
|
||||
return self.search(libtype=libtype, **kwargs)
|
||||
|
||||
def collection(self, **kwargs):
|
||||
""" Returns a list of collections from this library section. """
|
||||
key = '/library/sections/%s/collections' % self.key
|
||||
return self.fetchItems(key, **kwargs)
|
||||
""" Returns a list of collections from this library section.
|
||||
See description of :func:`plexapi.library.LibrarySection.search()` for details about filtering / sorting.
|
||||
"""
|
||||
return self.search(libtype='collection', **kwargs)
|
||||
|
||||
def playlist(self, **kwargs):
|
||||
""" Returns a list of playlists from this library section. """
|
||||
@@ -938,10 +952,17 @@ class MusicSection(LibrarySection):
|
||||
""" Search for a track. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
|
||||
return self.search(libtype='track', **kwargs)
|
||||
|
||||
def all(self, libtype='artist', **kwargs):
|
||||
""" Returns a list of all items from this library section.
|
||||
See description of :func:`plexapi.library.LibrarySection.search()` for details about filtering / sorting.
|
||||
"""
|
||||
return self.search(libtype=libtype, **kwargs)
|
||||
|
||||
def collection(self, **kwargs):
|
||||
""" Returns a list of collections from this library section. """
|
||||
key = '/library/sections/%s/collections' % self.key
|
||||
return self.fetchItems(key, **kwargs)
|
||||
""" Returns a list of collections from this library section.
|
||||
See description of :func:`plexapi.library.LibrarySection.search()` for details about filtering / sorting.
|
||||
"""
|
||||
return self.search(libtype='collection', **kwargs)
|
||||
|
||||
def playlist(self, **kwargs):
|
||||
""" Returns a list of playlists from this library section. """
|
||||
@@ -1009,6 +1030,12 @@ class PhotoSection(LibrarySection):
|
||||
""" Search for a photo. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
|
||||
return self.search(libtype='photo', title=title, **kwargs)
|
||||
|
||||
def all(self, libtype='photoalbum', **kwargs):
|
||||
""" Returns a list of all items from this library section.
|
||||
See description of :func:`plexapi.library.LibrarySection.search()` for details about filtering / sorting.
|
||||
"""
|
||||
return self.search(libtype=libtype, **kwargs)
|
||||
|
||||
def playlist(self, **kwargs):
|
||||
""" Returns a list of playlists from this library section. """
|
||||
key = '/playlists?type=15&playlistType=%s§ionID=%s' % (self.CONTENT_TYPE, self.key)
|
||||
@@ -1106,7 +1133,35 @@ class Hub(PlexObject):
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Collections(PlexObject):
|
||||
class Collections(PlexPartialObject):
|
||||
""" Represents a single Collection.
|
||||
Attributes:
|
||||
TAG (str): 'Directory'
|
||||
TYPE (str): 'collection'
|
||||
ratingKey (int): Unique key identifying this item.
|
||||
addedAt (datetime): Datetime this item was added to the library.
|
||||
childCount (int): Count of child object(s)
|
||||
collectionMode (str): How the items in the collection are displayed.
|
||||
collectionSort (str): How to sort the items in the collection.
|
||||
contentRating (str) Content rating (PG-13; NR; TV-G).
|
||||
fields (list): List of :class:`~plexapi.media.Field`.
|
||||
guid (str): Plex GUID (collection://XXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXX).
|
||||
index (int): Unknown
|
||||
key (str): API URL (/library/metadata/<ratingkey>).
|
||||
labels (List<:class:`~plexapi.media.Label`>): List of field objects.
|
||||
librarySectionID (int): :class:`~plexapi.library.LibrarySection` ID.
|
||||
librarySectionKey (str): API URL (/library/sections/<sectionkey>).
|
||||
librarySectionTitle (str): Section Title
|
||||
maxYear (int): Year
|
||||
minYear (int): Year
|
||||
subtype (str): Media type
|
||||
summary (str): Summary of the collection
|
||||
thumb (str): URL to thumbnail image.
|
||||
title (str): Collection Title
|
||||
titleSort (str): Title to use when sorting (defaults to title).
|
||||
type (str): Hardcoded 'collection'
|
||||
updatedAt (datatime): Datetime this item was updated.
|
||||
"""
|
||||
|
||||
TAG = 'Directory'
|
||||
TYPE = 'collection'
|
||||
@@ -1114,30 +1169,30 @@ class Collections(PlexObject):
|
||||
|
||||
def _loadData(self, data):
|
||||
self.ratingKey = utils.cast(int, data.attrib.get('ratingKey'))
|
||||
self._details_key = "/library/metadata/%s%s" % (self.ratingKey, self._include)
|
||||
self.key = data.attrib.get('key').replace('/children', '') # FIX_BUG_50
|
||||
self._details_key = self.key + self._include
|
||||
self.addedAt = utils.toDatetime(data.attrib.get('addedAt'))
|
||||
self.art = data.attrib.get('art')
|
||||
self.childCount = utils.cast(int, data.attrib.get('childCount'))
|
||||
self.collectionMode = utils.cast(int, data.attrib.get('collectionMode'))
|
||||
self.collectionSort = utils.cast(int, data.attrib.get('collectionSort'))
|
||||
self.contentRating = data.attrib.get('contentRating')
|
||||
self.fields = self.findItems(data, media.Field)
|
||||
self.guid = data.attrib.get('guid')
|
||||
self.key = data.attrib.get('key')
|
||||
self.index = utils.cast(int, data.attrib.get('index'))
|
||||
self.labels = self.findItems(data, media.Label)
|
||||
self.librarySectionID = data.attrib.get('librarySectionID')
|
||||
self.librarySectionKey = data.attrib.get('librarySectionKey')
|
||||
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
||||
self.type = data.attrib.get('type')
|
||||
self.title = data.attrib.get('title')
|
||||
self.titleSort = data.attrib.get('titleSort')
|
||||
self.maxYear = utils.cast(int, data.attrib.get('maxYear'))
|
||||
self.minYear = utils.cast(int, data.attrib.get('minYear'))
|
||||
self.subtype = data.attrib.get('subtype')
|
||||
self.summary = data.attrib.get('summary')
|
||||
self.index = utils.cast(int, data.attrib.get('index'))
|
||||
self.thumb = data.attrib.get('thumb')
|
||||
self.addedAt = utils.toDatetime(data.attrib.get('addedAt'))
|
||||
self.title = data.attrib.get('title')
|
||||
self.titleSort = data.attrib.get('titleSort')
|
||||
self.type = data.attrib.get('type')
|
||||
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
|
||||
self.childCount = utils.cast(int, data.attrib.get('childCount'))
|
||||
self.minYear = utils.cast(int, data.attrib.get('minYear'))
|
||||
self.maxYear = utils.cast(int, data.attrib.get('maxYear'))
|
||||
self.collectionMode = utils.cast(int, data.attrib.get('collectionMode'))
|
||||
self.collectionSort = utils.cast(int, data.attrib.get('collectionSort'))
|
||||
self.labels = self.findItems(data, media.Label)
|
||||
self.fields = self.findItems(data, media.Field)
|
||||
|
||||
@property
|
||||
def children(self):
|
||||
@@ -1162,14 +1217,12 @@ class Collections(PlexObject):
|
||||
|
||||
def modeUpdate(self, mode=None):
|
||||
""" Update Collection Mode
|
||||
|
||||
Parameters:
|
||||
mode: default (Library default)
|
||||
hide (Hide Collection)
|
||||
hideItems (Hide Items in this Collection)
|
||||
showItems (Show this Collection and its Items)
|
||||
Example:
|
||||
|
||||
collection = 'plexapi.library.Collections'
|
||||
collection.updateMode(mode="hide")
|
||||
"""
|
||||
@@ -1185,13 +1238,10 @@ class Collections(PlexObject):
|
||||
|
||||
def sortUpdate(self, sort=None):
|
||||
""" Update Collection Sorting
|
||||
|
||||
Parameters:
|
||||
sort: realease (Order Collection by realease dates)
|
||||
alpha (Order Collection Alphabetically)
|
||||
|
||||
Example:
|
||||
|
||||
colleciton = 'plexapi.library.Collections'
|
||||
collection.updateSort(mode="alpha")
|
||||
"""
|
||||
|
@@ -821,6 +821,27 @@ class Chapter(PlexObject):
|
||||
self.end = cast(int, data.attrib.get('endTimeOffset'))
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Marker(PlexObject):
|
||||
""" Represents a single Marker media tag.
|
||||
Attributes:
|
||||
TAG (str): 'Marker'
|
||||
"""
|
||||
TAG = 'Marker'
|
||||
|
||||
def __repr__(self):
|
||||
name = self._clean(self.firstAttr('type'))
|
||||
start = utils.millisecondToHumanstr(self._clean(self.firstAttr('start')))
|
||||
end = utils.millisecondToHumanstr(self._clean(self.firstAttr('end')))
|
||||
return '<%s:%s %s - %s>' % (self.__class__.__name__, name, start, end)
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
self.type = data.attrib.get('type')
|
||||
self.start = cast(int, data.attrib.get('startTimeOffset'))
|
||||
self.end = cast(int, data.attrib.get('endTimeOffset'))
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Field(PlexObject):
|
||||
""" Represents a single Field.
|
||||
|
@@ -38,7 +38,7 @@ class Photoalbum(PlexPartialObject):
|
||||
self.composite = data.attrib.get('composite')
|
||||
self.guid = data.attrib.get('guid')
|
||||
self.index = utils.cast(int, data.attrib.get('index'))
|
||||
self.key = data.attrib.get('key', '').replace('/children', '')
|
||||
self.key = data.attrib.get('key', '').replace('/children', '') # FIX_BUG_50
|
||||
self.librarySectionID = data.attrib.get('librarySectionID')
|
||||
self.librarySectionKey = data.attrib.get('librarySectionKey')
|
||||
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
||||
|
@@ -402,7 +402,7 @@ class Show(Video):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
Video._loadData(self, data)
|
||||
# fix key if loaded from search
|
||||
self.key = self.key.replace('/children', '')
|
||||
self.key = self.key.replace('/children', '') # FIX_BUG_50
|
||||
self.art = data.attrib.get('art')
|
||||
self.banner = data.attrib.get('banner')
|
||||
self.childCount = utils.cast(int, data.attrib.get('childCount'))
|
||||
@@ -699,6 +699,7 @@ class Episode(Playable, Video):
|
||||
self.labels = self.findItems(data, media.Label)
|
||||
self.collections = self.findItems(data, media.Collection)
|
||||
self.chapters = self.findItems(data, media.Chapter)
|
||||
self.markers = self.findItems(data, media.Marker)
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s>' % ':'.join([p for p in [
|
||||
@@ -730,6 +731,13 @@ class Episode(Playable, Video):
|
||||
""" Returns the s00e00 string containing the season and episode. """
|
||||
return 's%se%s' % (str(self.seasonNumber).zfill(2), str(self.index).zfill(2))
|
||||
|
||||
@property
|
||||
def hasIntroMarker(self):
|
||||
""" Returns True if this episode has an intro marker in the xml. """
|
||||
if not self.isFullObject():
|
||||
self.reload()
|
||||
return any(marker.type == 'intro' for marker in self.markers)
|
||||
|
||||
def season(self):
|
||||
"""" Return this episodes :func:`~plexapi.video.Season`.. """
|
||||
return self.fetchItem(self.parentKey)
|
||||
|
@@ -300,7 +300,7 @@ def initialize(config_file):
|
||||
# Check for new versions
|
||||
if CONFIG.CHECK_GITHUB_ON_STARTUP and CONFIG.CHECK_GITHUB:
|
||||
try:
|
||||
versioncheck.check_update()
|
||||
versioncheck.check_update(use_cache=True)
|
||||
except:
|
||||
logger.exception("Unhandled exception")
|
||||
LATEST_VERSION = CURRENT_VERSION
|
||||
@@ -334,18 +334,6 @@ def initialize(config_file):
|
||||
logger.error("Unable to write current release to file '%s': %s" %
|
||||
(release_file, e))
|
||||
|
||||
# Get the real PMS urls for SSL and remote access
|
||||
if CONFIG.PMS_TOKEN and CONFIG.PMS_IP and CONFIG.PMS_PORT:
|
||||
plextv.get_server_resources()
|
||||
|
||||
# Refresh the users list on startup
|
||||
if CONFIG.PMS_TOKEN and CONFIG.REFRESH_USERS_ON_STARTUP:
|
||||
users.refresh_users()
|
||||
|
||||
# Refresh the libraries list on startup
|
||||
if CONFIG.PMS_IP and CONFIG.PMS_TOKEN and CONFIG.REFRESH_LIBRARIES_ON_STARTUP:
|
||||
libraries.refresh_libraries()
|
||||
|
||||
# Store the original umask
|
||||
UMASK = os.umask(0)
|
||||
os.umask(UMASK)
|
||||
@@ -523,6 +511,9 @@ def start():
|
||||
global _STARTED
|
||||
|
||||
if _INITIALIZED:
|
||||
# Start refreshes on a separate thread
|
||||
threading.Thread(target=startup_refresh).start()
|
||||
|
||||
global SCHED
|
||||
SCHED = BackgroundScheduler(timezone=pytz.UTC)
|
||||
activity_handler.ACTIVITY_SCHED = BackgroundScheduler(timezone=pytz.UTC)
|
||||
@@ -535,12 +526,13 @@ def start():
|
||||
notification_handler.start_threads(num_threads=CONFIG.NOTIFICATION_THREADS)
|
||||
notifiers.check_browser_enabled()
|
||||
|
||||
# Schedule newsletters
|
||||
newsletter_handler.NEWSLETTER_SCHED.start()
|
||||
newsletter_handler.schedule_newsletters()
|
||||
|
||||
# Cancel processing exports
|
||||
exporter.cancel_exports()
|
||||
|
||||
if CONFIG.FIRST_RUN_COMPLETE:
|
||||
activity_pinger.connect_server(log=True, startup=True)
|
||||
|
||||
if CONFIG.SYSTEM_ANALYTICS:
|
||||
global TRACKER
|
||||
TRACKER = initialize_tracker()
|
||||
@@ -554,13 +546,27 @@ def start():
|
||||
|
||||
analytics_event(category='system', action='start')
|
||||
|
||||
# Schedule newsletters
|
||||
newsletter_handler.NEWSLETTER_SCHED.start()
|
||||
newsletter_handler.schedule_newsletters()
|
||||
|
||||
_STARTED = True
|
||||
|
||||
|
||||
def startup_refresh():
|
||||
# Get the real PMS urls for SSL and remote access
|
||||
if CONFIG.PMS_TOKEN and CONFIG.PMS_IP and CONFIG.PMS_PORT:
|
||||
plextv.get_server_resources()
|
||||
|
||||
# Connect server after server resource is refreshed
|
||||
if CONFIG.FIRST_RUN_COMPLETE:
|
||||
activity_pinger.connect_server(log=True, startup=True)
|
||||
|
||||
# Refresh the users list on startup
|
||||
if CONFIG.PMS_TOKEN and CONFIG.REFRESH_USERS_ON_STARTUP:
|
||||
users.refresh_users()
|
||||
|
||||
# Refresh the libraries list on startup
|
||||
if CONFIG.PMS_IP and CONFIG.PMS_TOKEN and CONFIG.REFRESH_LIBRARIES_ON_STARTUP:
|
||||
libraries.refresh_libraries()
|
||||
|
||||
|
||||
def sig_handler(signum=None, frame=None):
|
||||
if signum is not None:
|
||||
logger.info("Signal %i caught, saving and exiting...", signum)
|
||||
@@ -2043,7 +2049,7 @@ def dbcheck():
|
||||
# Update official mobile device flag
|
||||
for device_id, in c_db.execute('SELECT device_id FROM mobile_devices').fetchall():
|
||||
c_db.execute('UPDATE mobile_devices SET official = ? WHERE device_id = ?',
|
||||
[mobile_app.validate_device_id(device_id), device_id])
|
||||
[mobile_app.validate_onesignal_id(device_id), device_id])
|
||||
|
||||
# Upgrade mobile_devices table from earlier versions
|
||||
try:
|
||||
|
@@ -501,7 +501,8 @@ NOTIFICATION_PARAMETERS = [
|
||||
{'name': 'Tagline', 'type': 'str', 'value': 'tagline', 'description': 'A tagline for the media item.'},
|
||||
{'name': 'Rating', 'type': 'float', 'value': 'rating', 'description': 'The rating (out of 10) for the item.'},
|
||||
{'name': 'Critic Rating', 'type': 'int', 'value': 'critic_rating', 'description': 'The critic rating (%) for the item.', 'help_text': 'Ratings source must be Rotten Tomatoes for the Plex Movie agent'},
|
||||
{'name': 'Audience Rating', 'type': 'int', 'value': 'audience_rating', 'description': 'The audience rating (%) for the item.', 'help_text': 'Ratings source must be Rotten Tomatoes for the Plex Movie agent'},
|
||||
{'name': 'Audience Rating', 'type': 'float', 'value': 'audience_rating', 'description': 'The audience rating for the item.', 'help_text': 'Rating out of 10 for IMDB, percentage (%) for Rotten Tomatoes and TMDB.'},
|
||||
{'name': 'User Rating', 'type': 'float', 'value': 'user_rating', 'description': 'The user (star) rating (out of 10) for the item.'},
|
||||
{'name': 'Duration', 'type': 'int', 'value': 'duration', 'description': 'The duration (in minutes) for the item.'},
|
||||
{'name': 'Poster URL', 'type': 'str', 'value': 'poster_url', 'description': 'A URL for the movie, TV show, or album poster.'},
|
||||
{'name': 'Plex ID', 'type': 'str', 'value': 'plex_id', 'description': 'The Plex ID for the item.', 'example': 'e.g. 5d7769a9594b2b001e6a6b7e'},
|
||||
|
@@ -89,6 +89,7 @@ _CONFIG_DEFINITIONS = {
|
||||
'CHECK_GITHUB': (int, 'General', 1),
|
||||
'CHECK_GITHUB_INTERVAL': (int, 'General', 360),
|
||||
'CHECK_GITHUB_ON_STARTUP': (int, 'General', 1),
|
||||
'CHECK_GITHUB_CACHE_SECONDS': (int, 'Advanced', 3600),
|
||||
'CLEANUP_FILES': (int, 'General', 0),
|
||||
'CLOUDINARY_CLOUD_NAME': (str, 'Cloudinary', ''),
|
||||
'CLOUDINARY_API_KEY': (str, 'Cloudinary', ''),
|
||||
|
@@ -492,6 +492,7 @@ class Export(object):
|
||||
'grandparentThumb': None,
|
||||
'grandparentTitle': None,
|
||||
'guid': None,
|
||||
'hasIntroMarker': None,
|
||||
'index': None,
|
||||
'key': None,
|
||||
'lastViewedAt': helpers.datetime_to_iso,
|
||||
@@ -499,6 +500,11 @@ class Export(object):
|
||||
'librarySectionKey': None,
|
||||
'librarySectionTitle': None,
|
||||
'locations': None,
|
||||
'markers': {
|
||||
'end': None,
|
||||
'start': None,
|
||||
'type': None
|
||||
},
|
||||
'media': {
|
||||
'aspectRatio': None,
|
||||
'audioChannels': None,
|
||||
@@ -1179,11 +1185,12 @@ class Export(object):
|
||||
'rating', 'userRating', 'contentRating',
|
||||
'summary', 'guid', 'duration', 'durationHuman', 'type', 'index',
|
||||
'parentTitle', 'parentRatingKey', 'parentGuid', 'parentIndex',
|
||||
'grandparentTitle', 'grandparentRatingKey', 'grandparentGuid'
|
||||
'grandparentTitle', 'grandparentRatingKey', 'grandparentGuid', 'hasIntroMarker'
|
||||
],
|
||||
2: [
|
||||
'directors.tag', 'writers.tag',
|
||||
'fields.name', 'fields.locked'
|
||||
'fields.name', 'fields.locked',
|
||||
'markers.type', 'markers.start', 'markers.end'
|
||||
],
|
||||
3: [
|
||||
'art', 'thumb', 'key', 'chapterSource',
|
||||
|
@@ -122,7 +122,8 @@ def add_live_tv_library(refresh=False):
|
||||
if result and not refresh or not result and refresh:
|
||||
return
|
||||
|
||||
logger.info("Tautulli Libraries :: Adding Live TV library to the database.")
|
||||
if not refresh:
|
||||
logger.info("Tautulli Libraries :: Adding Live TV library to the database.")
|
||||
|
||||
section_keys = {'server_id': plexpy.CONFIG.PMS_IDENTIFIER,
|
||||
'section_id': common.LIVE_TV_SECTION_ID}
|
||||
|
@@ -831,12 +831,16 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
||||
child_count = 1
|
||||
grandchild_count = 1
|
||||
|
||||
rating = notify_params['rating'] or notify_params['audience_rating']
|
||||
|
||||
critic_rating = ''
|
||||
if notify_params['rating_image'].startswith('rottentomatoes://') and notify_params['rating']:
|
||||
if notify_params['rating_image'].startswith('rottentomatoes://') \
|
||||
and notify_params['rating']:
|
||||
critic_rating = helpers.get_percent(notify_params['rating'], 10)
|
||||
|
||||
audience_rating = ''
|
||||
if notify_params['audience_rating']:
|
||||
audience_rating = notify_params['audience_rating']
|
||||
if notify_params['audience_rating_image'].startswith(('rottentomatoes://', 'themoviedb://')) \
|
||||
and audience_rating:
|
||||
audience_rating = helpers.get_percent(notify_params['audience_rating'], 10)
|
||||
|
||||
now = arrow.now()
|
||||
@@ -1013,9 +1017,10 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
||||
'collections': ', '.join(notify_params['collections']),
|
||||
'summary': notify_params['summary'],
|
||||
'tagline': notify_params['tagline'],
|
||||
'rating': notify_params['rating'],
|
||||
'rating': rating,
|
||||
'critic_rating': critic_rating,
|
||||
'audience_rating': audience_rating,
|
||||
'user_rating': notify_params['user_rating'],
|
||||
'duration': duration,
|
||||
'poster_title': notify_params['poster_title'],
|
||||
'poster_url': notify_params['poster_url'],
|
||||
|
@@ -1819,9 +1819,6 @@ class GROWL(Notifier):
|
||||
logger.error("Tautulli Notifiers :: {name} notification failed: authentication error".format(name=self.NAME))
|
||||
return False
|
||||
|
||||
# Fix message
|
||||
body = body.encode(plexpy.SYS_ENCODING, "replace")
|
||||
|
||||
# Send it, including an image
|
||||
image_file = os.path.join(str(plexpy.PROG_DIR),
|
||||
"data/interfaces/default/images/logo-circle.png")
|
||||
|
@@ -48,6 +48,11 @@ def refresh_users():
|
||||
logger.info("Tautulli Users :: Requesting users list refresh...")
|
||||
result = plextv.PlexTV().get_full_users_list()
|
||||
|
||||
server_id = plexpy.CONFIG.PMS_IDENTIFIER
|
||||
if not server_id:
|
||||
logger.error("Tautulli Users :: No PMS identifier, cannot refresh users. Verify server in settings.")
|
||||
return
|
||||
|
||||
if result:
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
|
@@ -17,5 +17,5 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
PLEXPY_BRANCH = "beta"
|
||||
PLEXPY_RELEASE_VERSION = "v2.6.0-beta"
|
||||
PLEXPY_BRANCH = "master"
|
||||
PLEXPY_RELEASE_VERSION = "v2.6.0"
|
@@ -20,6 +20,7 @@ from __future__ import unicode_literals
|
||||
from future.builtins import next
|
||||
from future.builtins import str
|
||||
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
@@ -29,10 +30,12 @@ import tarfile
|
||||
import plexpy
|
||||
if plexpy.PYTHON2:
|
||||
import common
|
||||
import helpers
|
||||
import logger
|
||||
import request
|
||||
else:
|
||||
from plexpy import common
|
||||
from plexpy import helpers
|
||||
from plexpy import logger
|
||||
from plexpy import request
|
||||
|
||||
@@ -154,8 +157,8 @@ def get_version_from_file():
|
||||
return current_version, current_branch
|
||||
|
||||
|
||||
def check_update(scheduler=False, notify=False):
|
||||
check_github(scheduler=scheduler, notify=notify)
|
||||
def check_update(scheduler=False, notify=False, use_cache=False):
|
||||
check_github(scheduler=scheduler, notify=notify, use_cache=use_cache)
|
||||
|
||||
if not plexpy.CURRENT_VERSION:
|
||||
plexpy.UPDATE_AVAILABLE = None
|
||||
@@ -173,7 +176,7 @@ def check_update(scheduler=False, notify=False):
|
||||
plexpy.MAC_SYS_TRAY_ICON.change_tray_update_icon()
|
||||
|
||||
|
||||
def check_github(scheduler=False, notify=False):
|
||||
def check_github(scheduler=False, notify=False, use_cache=False):
|
||||
plexpy.COMMITS_BEHIND = 0
|
||||
|
||||
if plexpy.CONFIG.GIT_TOKEN:
|
||||
@@ -181,12 +184,16 @@ def check_github(scheduler=False, notify=False):
|
||||
else:
|
||||
headers = {}
|
||||
|
||||
# Get the latest version available from github
|
||||
logger.info('Retrieving latest version information from GitHub')
|
||||
url = 'https://api.github.com/repos/%s/%s/commits/%s' % (plexpy.CONFIG.GIT_USER,
|
||||
plexpy.CONFIG.GIT_REPO,
|
||||
plexpy.CONFIG.GIT_BRANCH)
|
||||
version = request.request_json(url, headers=headers, timeout=20, validator=lambda x: type(x) == dict)
|
||||
version = github_cache('version', use_cache=use_cache)
|
||||
if not version:
|
||||
# Get the latest version available from github
|
||||
logger.info('Retrieving latest version information from GitHub')
|
||||
url = 'https://api.github.com/repos/%s/%s/commits/%s' % (plexpy.CONFIG.GIT_USER,
|
||||
plexpy.CONFIG.GIT_REPO,
|
||||
plexpy.CONFIG.GIT_BRANCH)
|
||||
version = request.request_json(url, headers=headers, timeout=20,
|
||||
validator=lambda x: type(x) == dict)
|
||||
github_cache('version', github_data=version)
|
||||
|
||||
if version is None:
|
||||
logger.warn('Could not get the latest version from GitHub. Are you running a local development version?')
|
||||
@@ -204,13 +211,16 @@ def check_github(scheduler=False, notify=False):
|
||||
logger.info('Tautulli is up to date')
|
||||
return plexpy.LATEST_VERSION
|
||||
|
||||
logger.info('Comparing currently installed version with latest GitHub version')
|
||||
url = 'https://api.github.com/repos/%s/%s/compare/%s...%s' % (plexpy.CONFIG.GIT_USER,
|
||||
plexpy.CONFIG.GIT_REPO,
|
||||
plexpy.LATEST_VERSION,
|
||||
plexpy.CURRENT_VERSION)
|
||||
commits = request.request_json(url, headers=headers, timeout=20, whitelist_status_code=404,
|
||||
validator=lambda x: type(x) == dict)
|
||||
commits = github_cache('commits', use_cache=use_cache)
|
||||
if not commits:
|
||||
logger.info('Comparing currently installed version with latest GitHub version')
|
||||
url = 'https://api.github.com/repos/%s/%s/compare/%s...%s' % (plexpy.CONFIG.GIT_USER,
|
||||
plexpy.CONFIG.GIT_REPO,
|
||||
plexpy.LATEST_VERSION,
|
||||
plexpy.CURRENT_VERSION)
|
||||
commits = request.request_json(url, headers=headers, timeout=20, whitelist_status_code=404,
|
||||
validator=lambda x: type(x) == dict)
|
||||
github_cache('commits', github_data=commits)
|
||||
|
||||
if commits is None:
|
||||
logger.warn('Could not get commits behind from GitHub.')
|
||||
@@ -226,8 +236,13 @@ def check_github(scheduler=False, notify=False):
|
||||
if plexpy.COMMITS_BEHIND > 0:
|
||||
logger.info('New version is available. You are %s commits behind' % plexpy.COMMITS_BEHIND)
|
||||
|
||||
url = 'https://api.github.com/repos/%s/%s/releases' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO)
|
||||
releases = request.request_json(url, timeout=20, whitelist_status_code=404, validator=lambda x: type(x) == list)
|
||||
releases = github_cache('releases', use_cache=use_cache)
|
||||
if not releases:
|
||||
url = 'https://api.github.com/repos/%s/%s/releases' % (plexpy.CONFIG.GIT_USER,
|
||||
plexpy.CONFIG.GIT_REPO)
|
||||
releases = request.request_json(url, timeout=20, whitelist_status_code=404,
|
||||
validator=lambda x: type(x) == list)
|
||||
github_cache('releases', github_data=releases)
|
||||
|
||||
if releases is None:
|
||||
logger.warn('Could not get releases from GitHub.')
|
||||
@@ -391,6 +406,30 @@ def checkout_git_branch():
|
||||
plexpy.CONFIG.GIT_BRANCH))
|
||||
|
||||
|
||||
def github_cache(cache, github_data=None, use_cache=True):
|
||||
timestamp = helpers.timestamp()
|
||||
cache_filepath = os.path.join(plexpy.CONFIG.CACHE_DIR, 'github_{}.json'.format(cache))
|
||||
|
||||
if github_data:
|
||||
cache_data = {'github_data': github_data, '_cache_time': timestamp}
|
||||
try:
|
||||
with open(cache_filepath, 'w', encoding='utf-8') as cache_file:
|
||||
json.dump(cache_data, cache_file)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
if not use_cache:
|
||||
return
|
||||
try:
|
||||
with open(cache_filepath, 'r', encoding='utf-8') as cache_file:
|
||||
cache_data = json.load(cache_file)
|
||||
if timestamp - cache_data['_cache_time'] < plexpy.CONFIG.CHECK_GITHUB_CACHE_SECONDS:
|
||||
logger.debug('Using cached GitHub %s data', cache)
|
||||
return cache_data['github_data']
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def read_changelog(latest_only=False, since_prev_release=False):
|
||||
changelog_file = os.path.join(plexpy.PROG_DIR, 'CHANGELOG.md')
|
||||
|
||||
|
@@ -2649,13 +2649,28 @@ class WebInterface(object):
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth(member_of("admin"))
|
||||
def delete_sync_rows(self, client_id, sync_id, **kwargs):
|
||||
@addtoapi("delete_synced_item")
|
||||
def delete_sync_rows(self, client_id=None, sync_id=None, **kwargs):
|
||||
""" Delete a synced item from a device.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
client_id (str): The client ID of the device to delete from
|
||||
sync_id (str): The sync ID of the synced item
|
||||
|
||||
Optional parameters:
|
||||
None
|
||||
|
||||
Returns:
|
||||
None
|
||||
```
|
||||
"""
|
||||
if client_id and sync_id:
|
||||
plex_tv = plextv.PlexTV()
|
||||
delete_row = plex_tv.delete_sync(client_id=client_id, sync_id=sync_id)
|
||||
return {'message': 'Sync deleted'}
|
||||
return {'result': 'success', 'message': 'Synced item deleted successfully.'}
|
||||
else:
|
||||
return {'message': 'no data received'}
|
||||
return {'result': 'error', 'message': 'Missing client ID and sync ID.'}
|
||||
|
||||
|
||||
##### Logs #####
|
||||
@@ -5032,12 +5047,13 @@ class WebInterface(object):
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth(member_of("admin"))
|
||||
@addtoapi("get_metadata")
|
||||
def get_metadata_details(self, rating_key='', **kwargs):
|
||||
def get_metadata_details(self, rating_key='', sync_id='', **kwargs):
|
||||
""" Get the metadata for a media item.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
rating_key (str): Rating key of the item
|
||||
rating_key (str): Rating key of the item, OR
|
||||
sync_id (str): Sync ID of a synced item
|
||||
|
||||
Optional parameters:
|
||||
None
|
||||
@@ -5188,7 +5204,8 @@ class WebInterface(object):
|
||||
```
|
||||
"""
|
||||
pms_connect = pmsconnect.PmsConnect()
|
||||
metadata = pms_connect.get_metadata_details(rating_key=rating_key)
|
||||
metadata = pms_connect.get_metadata_details(rating_key=rating_key,
|
||||
sync_id=sync_id)
|
||||
|
||||
if metadata:
|
||||
return metadata
|
||||
@@ -6528,6 +6545,31 @@ class WebInterface(object):
|
||||
|
||||
return status
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@addtoapi()
|
||||
def server_status(self, *args, **kwargs):
|
||||
""" Get the current status of Tautulli's connection to the Plex server.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
None
|
||||
|
||||
Optional parameters:
|
||||
None
|
||||
|
||||
Returns:
|
||||
json:
|
||||
{"result": "success",
|
||||
"connected": true,
|
||||
}
|
||||
```
|
||||
"""
|
||||
cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store"
|
||||
status = {'result': 'success', 'connected': plexpy.PLEX_SERVER_UP}
|
||||
|
||||
return status
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth(member_of("admin"))
|
||||
|
19
start.sh
@@ -1,21 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if [[ "$TAUTULLI_DOCKER" == "True" ]]; then
|
||||
if [[ -n $PUID && -n $PGID ]]; then
|
||||
getent group "$PGID" 2>&1 > /dev/null || groupadd -g "$PGID" tautulli
|
||||
getent passwd "$PUID" 2>&1 > /dev/null || useradd -r -u "$PUID" -g "$PGID" tautulli
|
||||
PUID=${PUID:-1000}
|
||||
PGID=${PGID:-1000}
|
||||
|
||||
user=$(getent passwd "$PUID" | cut -d: -f1)
|
||||
group=$(getent group "$PGID" | cut -d: -f1)
|
||||
usermod -a -G root "$user"
|
||||
groupmod -o -g "$PGID" tautulli
|
||||
usermod -o -u "$PUID" tautulli
|
||||
|
||||
chown -R "$user":"$group" /config
|
||||
chown -R tautulli:tautulli /config
|
||||
|
||||
echo "Running Tautulli using user $user (uid=$PUID) and group $group (gid=$PGID)"
|
||||
su "$user" -g "$group" -c "python /app/Tautulli.py --datadir /config"
|
||||
else
|
||||
python Tautulli.py --datadir /config
|
||||
fi
|
||||
echo "Running Tautulli using user tautulli (uid=$(id -u tautulli)) and group tautulli (gid=$(id -g tautulli))"
|
||||
exec gosu tautulli "$@"
|
||||
else
|
||||
python_versions=("python3" "python3.8" "python3.7" "python3.6" "python" "python2" "python2.7")
|
||||
for cmd in "${python_versions[@]}"; do
|
||||
|