diff --git a/CHANGELOG.md b/CHANGELOG.md index 781a3886..a749bf1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## v1.1.6 (2015-09-06) + +* Home stats cards are now expandable to show multiple items. Configurable in settings. Thanks @JonnyWong. +* Completely redesigned media info pages. Thanks @JonnyWong. +* Redesigned activity pane to match Plex Web more closely. Thanks @JonnyWong. +* New Library stats on home page, shows total item counts per library. Thanks @JonnyWong. +* New last watched card in home stats. Shows last watched items. Thanks @JonnyWong. +* Improved some layout issues on mobile devices. Thanks @JonnyWong. +* Fixed issue where some clip/channel items are reported as episodes and causing exceptions. +* Many styling improvements and fixes. Thanks @JonnyWong. +* Fixed incorrect sort on home stats platform count by duration. Thanks @JonnyWong. +* Fix issue where user refresh would continually be called as "Local" user didn't exist in database. +* Fixed styling on graph stream modal. Thanks @JonnyWong. +* Fixed some issues with users page editing. Thanks @JonnyWong. +* Fix error page when clicking through to an item that no longer exists. + + ## v1.1.5 (2015-08-27) * Fix git tag being one release behind. diff --git a/data/interfaces/default/base.html b/data/interfaces/default/base.html index 3bffc64a..f0918641 100644 --- a/data/interfaces/default/base.html +++ b/data/interfaces/default/base.html @@ -1,4 +1,4 @@ -<% +<% import plexpy from plexpy import version %> @@ -52,7 +52,7 @@ from plexpy import version - PlexPy + PlexPy ${next.headerIncludes()} -${next.body()} +
+ ${next.body()} +
diff --git a/data/interfaces/default/css/plexpy.css b/data/interfaces/default/css/plexpy.css index 587bee7b..d8379a65 100644 --- a/data/interfaces/default/css/plexpy.css +++ b/data/interfaces/default/css/plexpy.css @@ -1,7 +1,8 @@ body { font-family: 'Open Sans', sans-serif; color: #fff; - padding-top: 50px; + margin-top: 50px; + overflow: hidden; } a { color: #eee; @@ -39,6 +40,9 @@ img { } .navbar { background: #000; + -webkit-box-shadow: 0 0 0 3px rgba(0,0,0,.2); + -moz-box-shadow: 0 0 0 3px rgba(0,0,0,.2); + box-shadow: 0 0 0 3px rgba(0,0,0,.2); } .navbar-brand { padding: 5px 5px; @@ -80,36 +84,10 @@ img { .padded-header h3 { font-size: 20px; } -.activity-progress { - overflow: hidden; - height: 4px; - background-color: #111; - float: left; - width: 100%; - margin: 0 10px 5px 0; -} -.activity-progress .bar { - background-color: #faa732; - background-image: -moz-linear-gradient(top, #fbb450, #f89406); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); - background-image: -webkit-linear-gradient(top, #fbb450, #f89406); - background-image: -o-linear-gradient(top, #fbb450, #f89406); - background-image: linear-gradient(to bottom, #fbb450, #f89406); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0); -} .btn { outline:0px !important; -webkit-appearance:none; } -.btn-activity-info, -.btn-activity-info:focus { - color: #aaa; - background-color: rgba(0,0,0,0.5); -} -.btn-activity-info:hover { - color: #eee; -} .btn-dark { color: #d7d7d7; background-color: #3B3B3B; @@ -296,6 +274,10 @@ fieldset[disabled] .btn-bright.active { .modal-body i.fa { color: #fff; } +.modal-body td:hover a .fa, +.modal-body a:focus i.fa { + color: #f9aa03; +} .modal-body strong { color: #F9AA03; } @@ -432,11 +414,8 @@ input[type="color"], pointer-events: none; } .poster { - float: left; - min-height: 225px; - min-width: 150px; - margin-bottom: 8px; position: relative; + height: 225px; } .poster-face { background-position: center; @@ -444,16 +423,11 @@ input[type="color"], height: 225px; width: 150px; position: relative; - webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); + -webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); -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); overflow: hidden; } -a .poster-face:hover { - webkit-box-shadow: inset 0 0 0 2px #e9a049; - -moz-box-shadow: inset 0 0 0 2px #e9a049; - box-shadow: inset 0 0 0 2px #e9a049; -} .cover-face { background-position: center; background-size: cover; @@ -461,20 +435,10 @@ a .poster-face:hover { width: 150px; position: absolute; bottom: 0; - webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); + -webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); -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); } -a .cover-face:hover { - webkit-box-shadow: inset 0 0 0 2px #e9a049; - -moz-box-shadow: inset 0 0 0 2px #e9a049; - box-shadow: inset 0 0 0 2px #e9a049; -} -a .users-poster-face:hover { - webkit-box-shadow: inset 0 0 0 2px #e9a049; - -moz-box-shadow: inset 0 0 0 2px #e9a049; - box-shadow: inset 0 0 0 2px #e9a049; -} .users-poster-face { overflow: hidden; float: left; @@ -484,28 +448,16 @@ a .users-poster-face:hover { -webkit-border-radius: 50%; -moz-border-radius: 50%; border-radius: 50%; - webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); + -webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); -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); } -.users-poster-face img { - bottom: 0; - overflow: hidden; - float: left; - background-color: #323232; - background-position: center; - background-size: cover; - -webkit-border-radius: 1000px; - -moz-border-radius: 1000px; - border-radius: 1000px; - -webkit-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); - width: 40px; - height: 40px; -} -.users-poster-face img:hover{ - -webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 2px #F9AA03; - box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 2px #F9AA03; +a .poster-face:hover, +a .cover-face:hover, +a .users-poster-face:hover { + webkit-box-shadow: inset 0 0 0 2px #e9a049; + -moz-box-shadow: inset 0 0 0 2px #e9a049; + box-shadow: inset 0 0 0 2px #e9a049; } .users-name { position: relative; @@ -516,54 +468,184 @@ a .users-poster-face:hover { margin-left: 5px; float: left; } -.dashboard-activity-metadata-wrapper { +.dashboard-instance { + float: left; position: relative; - left: 0px; - width: 250px; - font-size: 13px; + height: 260px; + width: 350px; + margin-right: 20px; + margin-bottom: 20px; } -.dashboard-activity-button-info { - position: absolute; - top: 3px; - left: 5px; - z-index: 1; +.dashboard-activity-poster { + height: 200px; + width: 100%; + position: relative; + overflow: hidden; +} +a:hover .dashboard-activity-poster { + -webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 2px #e9a049; + -moz-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 2px #e9a049; + box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 2px #e9a049; } .dashboard-activity-poster-face { - height: 141px; - width: 250px; - position: relative; - top: 0px; - background-color: rgba(255,255,255,.03); - margin-bottom: 11px; - text-align: center; -} -.dashboard-activity-poster-face img { - bottom: 0; - overflow: hidden; - height: 140px; - min-width: 140px; - max-width: 250px; -} -.dashboard-activity-poster-music-bg { background-position: center; - position:absolute; - width:100%; - height:100%; - opacity: 0.7; - z-index: -999; + background-size: cover; + height: 100%; + width: 100%; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + -webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); + -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); + z-index: -3; +} +.dashboard-activity-cover-face-bg { + background-position: center; + background-size: cover; + background-repeat: no-repeat; + height: 100%; + width: 100%; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + -webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); + -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); + -webkit-filter: blur(5px); + -moz-filter: blur(5px); + filter: blur(5px); + opacity: 0.75; + z-index: -3; +} +.dashboard-activity-cover-face { + background-position: center; + background-size: cover; + background-repeat: no-repeat; + height: 100%; + width: 200px; + -webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); + -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); + position: absolute; + top: 0; + left: calc(50% - 100px); + z-index: -3; +} +.dashboard-activity-clip-face-bg { + background-position: center; + background-size: cover; + background-repeat: no-repeat; + height: 100%; + width: 100%; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + -webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); + -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); + -webkit-filter: blur(5px); + -moz-filter: blur(5px); + filter: blur(5px); + opacity: 0.75; + z-index: -3; +} +.dashboard-activity-clip-face { + background-position: center; + background-size: cover; + background-repeat: no-repeat; + height: 100%; + width: 130px; + position: relative; + -webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); + -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); + position: absolute; + top: 0; + left: calc(50% - 65px); + z-index: -3; +} +.dashboard-activity-info-details-overlay { + text-align: left; + background-image: -webkit-gradient(linear,left 0,left 100%,from(rgba(0,0,0,.6)),to(rgba(0,0,0,.8))); + background-image: -webkit-linear-gradient(top,rgba(0,0,0,.6),0,rgba(0,0,0,.8),100%); + background-image: -moz-linear-gradient(top,rgba(0,0,0,.6) 0,rgba(0,0,0,.8) 100%); + background-image: linear-gradient(to bottom,rgba(0,0,0,.6) 0,rgba(0,0,0,.8) 100%); + background-repeat: repeat-x; + position: absolute; + top: 0; + width: 100%; + height: 100%; + display: none; + z-index: -1; +} +.dashboard-activity-button-info { + width: 50px; + height: 50px; + position: absolute; + top: 10px; + right: 10px; + z-index: 1; +} +.btn-activity-info, +.btn-activity-info:focus { + color: #999; + background-color: rgba(0,0,0,0.5); +} +.btn-activity-info:hover { + color: #fff; +} +.dashboard-activity-info-details-content { + height: 200px; + width: 100%; + line-height: 25px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + color: #aaa; + padding: 10px; +} +.dashboard-activity-info-details-content strong { + color: #fff; + font-weight: normal; +} +.dashboard-activity-info-platform { + width: 100%; + height: 50px; + margin-bottom: 10px; +} +.dashboard-activity-info-platform-box { + float: left; + background-position: center; + background-size: cover; + width: 50px; + height: 50px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + margin-right: 10px; } .dashboard-activity-poster-info-bar { position:absolute; - color: #F9AA03; - bottom: 0; - width:100%; - height: 100px; - background: -moz-linear-gradient(top, rgba(0,0,0,0) 30%, rgba(0,0,0,1) 100%); - background: -webkit-gradient(linear, left top, left bottom, color-stop(30%,rgba(0,0,0,0)), color-stop(100%,rgba(0,0,0,1))); - background: -webkit-linear-gradient(top, rgba(0,0,0,0) 30%,rgba(0,0,0,1) 100%); - background: -o-linear-gradient(top, rgba(0,0,0,0) 30%,rgba(0,0,0,1) 100%); - background: -ms-linear-gradient(top, rgba(0,0,0,0) 30%,rgba(0,0,0,1) 100%); - background: linear-gradient(to bottom, rgba(0,0,0,0) 30%,rgba(0,0,0,1) 100%); + width: 100%; + height: 100%; + background-image: -webkit-gradient(linear,left 30%,left 100%,from(rgba(0,0,0,0)),to(rgba(0,0,0,0.75))); + background-image: -webkit-linear-gradient(top,rgba(0,0,0,0),30%,rgba(0,0,0,0.75),100%); + background-image: -moz-linear-gradient(top,rgba(0,0,0,0) 30%,rgba(0,0,0,0.75) 100%); + background-image: linear-gradient(to bottom,rgba(0,0,0,0) 30%,rgba(0,0,0,0.75) 100%); + opacity: 0; + -webkit-transition: all .2s; + transition: all .2s; + z-index: -2; +} +.dashboard-activity-poster:hover .dashboard-activity-poster-info-bar { + opacity: 1; } .dashboard-activity-poster-info-text { position: absolute; @@ -581,90 +663,119 @@ a .users-poster-face:hover { } .dashboard-activity-poster-info-time { position: absolute; - bottom: 0; - right: 0; - float: right; + bottom: 5px; + right: 10px; text-align: right; - padding: 5px 8px; font-size: 12px; color: #eee; } -.dashboard-activity-metadata-user { - text-overflow: ellipsis; +.dashboard-activity-progress { + width: 100%; + height: 5px; + margin-bottom: 5px; +} +.dashboard-activity-progress-bar { overflow: hidden; - white-space: nowrap; - width: 80%; - margin-right: 5px; - float: left; + height: 5px; + background-color: #111; + width: 100%; + z-index: -1; + -webkit-transition: all 0s; + transition: all 0s; } -.dashboard-activity-metadata-user a { - color: #F9AA03; +.dashboard-activity-progress .bar { + padding-top: 6px; + background-color: #faa732; + background-image: -moz-linear-gradient(top, #fbb450, #f89406); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); + background-image: -webkit-linear-gradient(top, #fbb450, #f89406); + background-image: -o-linear-gradient(top, #fbb450, #f89406); + background-image: linear-gradient(to bottom, #fbb450, #f89406); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0); } -.dashboard-activity-metadata-user a:hover { - color: #FFCD62; +.dashboard-activity-metadata-wrapper { + position: relative; + width: 100%; + height: 50px; + font-size: 13px; + padding: 0 3px; } .dashboard-activity-metadata-title { text-overflow: ellipsis; overflow: hidden; white-space: nowrap; font-size: 14px; - width: 80%; - margin-right: 5px; + line-height: 25px; color: #fff; + max-width: 300px; +} +.dashboard-activity-metadata-subtitle { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + font-size: 14px; + line-height: 25px; + color: #999; + max-width: 172px; float: left; } +.dashboard-activity-metadata-user { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + font-size: 14px; + line-height: 25px; + color: #999; + text-align: right; + max-width: 122px; + float: right; +} +.dashboard-activity-metadata-user-thumb { + background-position: center; + background-size: cover; + margin-top: 5px; + margin-left: 10px; + height: 40px; + width: 40px; + position: relative; + -webkit-border-radius: 50%; + -moz-border-radius: 50%; + border-radius: 50%; + -webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); + -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); + overflow: hidden; + float: right; +} +a .dashboard-activity-metadata-user-thumb:hover { + -webkit-box-shadow: inset 0 0 0 2px #e9a049; + -moz-box-shadow: inset 0 0 0 2px #e9a049; + box-shadow: inset 0 0 0 2px #e9a049; +} +.dashboard-activity-metadata-user a { + color: #999; +} +.dashboard-activity-metadata-user a:hover { + color: #F9AA03; +} .dashboard-activity-metadata-title a:hover { color: #F9AA03; } -.dashboard-activity-metadata-platform { - float: right; -} -.dashboard-activity-metadata-platform img { - width: 40px; - height: 40px; -} .dashboard-activity-metadata-progress-wrapper { margin-bottom: 20px; font-size: 12px; font-weight: bold; color: #F9AA03; } -.dashboard-activity-metadata-progress-minutes { - width: 100%; -} -.dashboard-activity-instance-overlay { +/*.dashboard-activity-instance-overlay { position: relative; top: -12px; text-align: left; height: 53px; width: 250px; border-radius: 0 0 3px 3px; -} -.dashboard-activity-info-details-overlay { - text-align: left; - background-color: rgba(0,0,0,0.5); - width: 250px; - padding: 5px 0px 5px 0px; -} -.dashboard-activity-info-details-content { - margin-left: 5px; - margin-right: 5px; - width: 245px; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - color: #999; -} -.dashboard-activity-info-details-content strong { - color: #fff; - font-weight: normal; -} -.instance { - padding-bottom: 15px; - float: left; - position: relative; - margin-right: 27px; -} +}*/ .dashboard-recent-media-row { width: 100%; margin:0 auto; @@ -683,8 +794,8 @@ a .users-poster-face:hover { float: left; min-height: 340px; } -.dashboard-recent-media-metacontainer{ - width: 153px; +.dashboard-recent-media-metacontainer { + width: 150px; font-size: 13px; margin-bottom: 20px; clear: both; @@ -699,7 +810,7 @@ a .users-poster-face:hover { margin: 0; line-height: 15px; font-weight: normal; - width: 153px; + width: 150px; white-space: nowrap; text-align: left; clear: both; @@ -714,244 +825,213 @@ a .users-poster-face:hover { } .art-face { background-repeat: no-repeat; - background-position: 50% 0%; - background-size: 100%; - height: 540px; - overflow: hidden; - min-width: 280px; - position: relative; - -webkit-border-radius: 2px; - -moz-border-radius: 2px; - border-radius: 2px; - -webkit-box-shadow: 0 0 0px rgba(0,0,0,0.75); - -moz-box-shadow: 0 0 0px rgba(0,0,0,0.75); - box-shadow: 0 0 0px rgba(0,0,0,0.75); - -webkit-transform: translateZ(0); - -moz-transform: translateZ(0); - -ms-transform: translateZ(0); - -o-transform: translateZ(0); - transform: translateZ(0); - -webkit-transform-style: preserve-3d; - -moz-transform-style: preserve-3d; - -ms-transform-style: preserve-3d; - -o-transform-style: preserve-3d; - transform-style: preserve-3d; - -webkit-perspective: 1000; - -moz-perspective: 1000; - -ms-perspective: 1000; - -o-perspective: 1000; - perspective: 1000; - -webkit-backface-visibility: hidden; - -moz-backface-visibility: hidden; - -ms-backface-visibility: hidden; - -o-backface-visibility: hidden; - backface-visibility: hidden; - clear: both; -} -.art-face img { - -webkit-border-radius: 2px; - -moz-border-radius: 2px; - border-radius: 2px; - border-color: #1d1d1d; - -webkit-border-radius: 2px; - -moz-border-radius: 2px; - border-radius: 2px; - -webkit-box-shadow: 0 0 10px rgba(0,0,0,0.75); - -moz-box-shadow: 0 0 10px rgba(0,0,0,0.75); - box-shadow: 0 0 10px rgba(0,0,0,0.75); - -webkit-transform: translateZ(0); - -moz-transform: translateZ(0); - -ms-transform: translateZ(0); - -o-transform: translateZ(0); - transform: translateZ(0); - -webkit-transform-style: preserve-3d; - -moz-transform-style: preserve-3d; - -ms-transform-style: preserve-3d; - -o-transform-style: preserve-3d; - transform-style: preserve-3d; - -webkit-perspective: 1000; - -moz-perspective: 1000; - -ms-perspective: 1000; - -o-perspective: 1000; - perspective: 1000; - -webkit-backface-visibility: hidden; - -moz-backface-visibility: hidden; - -ms-backface-visibility: hidden; - -o-backface-visibility: hidden; - backface-visibility: hidden; + background-position: center center; + background-attachment: scroll; + background-size: cover; + opacity: 0; + position: absolute; + top: 0; + bottom: 0; + width: 100%; } .art-music-face { background-repeat: no-repeat; - background-position: 50% 50%; - background-size: 100%; + background-position: center; + background-size: cover; width: 250px; - height: 141px; - overflow: hidden; position: relative; - -webkit-border-radius: 2px; - -moz-border-radius: 2px; - border-radius: 2px; - -webkit-box-shadow: 0 0 0px rgba(0,0,0,0.75); - -moz-box-shadow: 0 0 0px rgba(0,0,0,0.75); - box-shadow: 0 0 0px rgba(0,0,0,0.75); - -webkit-transform: translateZ(0); - -moz-transform: translateZ(0); - -ms-transform: translateZ(0); - -o-transform: translateZ(0); - transform: translateZ(0); - -webkit-transform-style: preserve-3d; - -moz-transform-style: preserve-3d; - -ms-transform-style: preserve-3d; - -o-transform-style: preserve-3d; - transform-style: preserve-3d; - -webkit-perspective: 1000; - -moz-perspective: 1000; - -ms-perspective: 1000; - -o-perspective: 1000; - perspective: 1000; - -webkit-backface-visibility: hidden; - -moz-backface-visibility: hidden; - -ms-backface-visibility: hidden; - -o-backface-visibility: hidden; - backface-visibility: hidden; - clear: both; + top: 0; + bottom: 0; } -.summary-wrapper { - height: auto; - width: 100%; +.summary-container { position: absolute; + top: 0; + right: 0; bottom: 0; + left: 0; + overflow-x: hidden; + overflow-y: auto; } -.summary-overlay { - background-color: rgba(0,0,0,0.85); - color: #fff; - font-size: 14px; - height: auto; - position: relative; - top: 0px; - border-color: #1d1d1d; - border: 2px solid #191919; +.summary-container .table-card-header, +.summary-container .table-card-back { + opacity: 0.90; } -.summary-content-wrapper { - margin-left: auto; - margin-right: auto; - width: 960px; -} -.summary-content-poster { - position: relative; - top: -10px; - float: left; - margin-left: 25px; - width: 150px; - height: 225px; -} -.summary-content-poster img { - bottom: 0; +.summary-navbar { + background-color: rgba(255,255,255,.03); + height: 50px; + line-height: 50px; + max-height: 50px; overflow: hidden; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; - -webkit-box-shadow: 0 0 10px rgba(0,0,0,0.75); - -moz-box-shadow: 0 0 10px rgba(0,0,0,0.75); - box-shadow: 0 0 10px rgba(0,0,0,0.75); - -webkit-transform: translateZ(0); - -moz-transform: translateZ(0); - -ms-transform: translateZ(0); - -o-transform: translateZ(0); - transform: translateZ(0); - -webkit-transform-style: preserve-3d; - -moz-transform-style: preserve-3d; - -ms-transform-style: preserve-3d; - -o-transform-style: preserve-3d; - transform-style: preserve-3d; - -webkit-perspective: 1000; - -moz-perspective: 1000; - -ms-perspective: 1000; - -o-perspective: 1000; - perspective: 1000; - -webkit-backface-visibility: hidden; - -moz-backface-visibility: hidden; - -ms-backface-visibility: hidden; - -o-backface-visibility: hidden; - backface-visibility: hidden; } -.summary-content { - position: relative; - top: 0px; - left: 20px; +.summary-navbar-list { + padding: 0 25px; color: #999; - height: auto; - overflow: hidden; - margin-right: 20px; - margin-bottom: 20px; } -.summary-content-title h1{ - margin-top: 15px; - margin-bottom: 15px; - color: #F9AA03; - font-size: 24px; - line-height: 32px; +.summary-navbar-list span { float: left; + margin-right: 20px; } -.summary-content-title h1 a{ +.summary-navbar-list span a { + color: #999; +} +.summary-navbar-list span a:hover { color: #F9AA03; } -.summary-content-title h1 a:hover{ +.summary-content-title-wrapper { + height: 150px; + padding-top: 50px; +} +.summary-content-title { + overflow: hidden; + position: relative; + max-height: 100px; +} +.summary-content-title h1 { + margin-top: 0; + margin-bottom: 10px; + color: #F9AA03; + font-size: 28px; + line-height: 40px; + float: left; + clear: left; +} +.summary-content-title h1 a { + color: #F9AA03; +} +.summary-content-title h1 a:hover { color: #F9AA03; text-decoration: underline; } +.summary-content-title h2 { + margin-top: 0; + margin-bottom: 10px; + color: #fff; + font-size: 28px; + line-height: 40px; + float: left; + clear: left; +} +.summary-content-title h3 { + margin-top: 0; + margin-bottom: 10px; + color: #999; + font-size: 28px; + line-height: 40px; + float: right; +} +.summary-content-title h3 a:hover { + text-decoration: underline; +} +.summary-content-title h3 a { + color: #999; +} +.summary-content-wrapper { + background-image: linear-gradient(rgba(0,0,0,.4),rgba(19,19,19,.4) 50%,rgba(26,26,26,.4)); + background-clip: content-box; + min-height: calc(100% - 200px); + position: inherit; + width: 100%; +} +.summary-content-poster { + float: left; + width: 250px; + margin: 0 40px 0 25px; + height: 100px; + overflow: visible; +} +.summary-poster-face { + background-position: center; + background-size: cover; + height: 375px; + width: 250px; + position: relative; + webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); + -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); + overflow: hidden; + z-index: 1; +} +.summary-poster-face-episode { + background-position: center; + background-size: cover; + height: 140px; + width: 250px; + position: relative; + webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); + -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); + overflow: hidden; + z-index: 1; +} +a .summary-poster-face:hover, +a .summary-poster-face-episode:hover { + webkit-box-shadow: inset 0 0 0 2px #e9a049; + -moz-box-shadow: inset 0 0 0 2px #e9a049; + box-shadow: inset 0 0 0 2px #e9a049; +} +.summary-content-padding { + float: left; + width: 250px; + height: 1px; + margin: 0 40px 20px 25px; +} +.summary-content { + position: relative; + color: #999; + overflow: hidden; + padding-right: 10px; + margin-bottom: 20px; +} .summary-content-details-wrapper { width: 100%; - padding-bottom: 15px; - margin-top: -10px; - clear: both; + padding: 10px 0 15px 0; position: relative; - top: -15px; + float: left; } .summary-content-director { float: left; - margin-right: 10px; line-height: 24px; } .summary-content-director strong { color: #fff; margin-left: 2px; + margin-right: 10px; } .summary-content-studio { float: left; - margin-right: 10px; line-height: 24px; } .summary-content-studio strong { color: #fff; margin-left: 2px; + margin-right: 10px; } .summary-content-airdate { float: left; - margin-right: 10px; line-height: 24px; } .summary-content-airdate strong { color: #fff; margin-left: 2px; + margin-right: 10px; } .summary-content-duration { float: left; - margin-right: 10px; line-height: 24px; } .summary-content-duration strong { color: #fff; margin-left: 2px; + margin-right: 10px; } .summary-content-content-rating { float: left; line-height: 24px; - margin-right: 10px; } .summary-content-content-rating strong { color: #fff; margin-left: 2px; + margin-right: 10px; } .summary-content-summary { overflow: hidden; @@ -959,14 +1039,12 @@ a .users-poster-face:hover { float: left; position: relative; clear: both; - padding-right: 10px; height: auto; max-height: 160px; padding-bottom: 0px; } .summary-content-people-wrapper { - margin-top: 25px; - margin-right: 25px; + margin-right: 20px; float: left; } .summary-content-people-wrapper hidden-phone hidden-tablet { @@ -975,9 +1053,7 @@ a .users-poster-face:hover { min-height: 0px; } .summary-content-actors { - margin-top: 0px; - margin-left: 0px; - margin-right: 15px; + margin-top: 13px; font-size: 12px; line-height: 18px; color: #999; @@ -985,6 +1061,7 @@ a .users-poster-face:hover { } .summary-content-actors ul { padding-left:20px; + margin-bottom: 0; } .summary-content-actors li { list-style: none; @@ -995,9 +1072,7 @@ a .users-poster-face:hover { color: #fff; } .summary-content-genres { - margin-top: 0px; - margin-left: 0px; - margin-right: 15px; + margin-top: 13px; font-size: 12px; line-height: 18px; color: #999; @@ -1005,6 +1080,7 @@ a .users-poster-face:hover { } .summary-content-genres ul { padding-left:20px; + margin-bottom: 0; } .summary-content-genres li { list-style: none; @@ -1015,9 +1091,7 @@ a .users-poster-face:hover { color: #fff; } .summary-content-writers { - margin-top: 0px; - margin-left: 0px; - margin-right: 15px; + margin-top: 13px; font-size: 12px; line-height: 18px; color: #999; @@ -1025,6 +1099,7 @@ a .users-poster-face:hover { } .summary-content-writers ul { padding-left: 20px; + margin-bottom: 0; } .summary-content-writers li { list-style: none; @@ -1037,7 +1112,6 @@ a .users-poster-face:hover { .rateit { display: -moz-inline-box; display: inline-block; - position: relative; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; @@ -1045,33 +1119,121 @@ a .users-poster-face:hover { -ms-user-select: none; user-select: none; -webkit-touch-callout: none; - float: right; - margin-top: 23px; - margin-right: 20px; overflow: hidden; white-space: nowrap; + float: right; + margin-top: 3px; + height: 21px; } .rateit .rateit-range { - background: url(../images/star-gray-16.png); + background: url(../images/star-gray-32.png); + background-size: contain; height: 16px; } .rateit .rateit-selected { - background: url(../images/star-16.png); + background: url(../images/star-32.png); + background-size: contain; + height: 16px; +} +.show-seasons-wrapper { +} +.show-seasons-instance { + list-style: none; + margin: 0; +} +.show-seasons-instance li { + float: left; + position: relative; + left: 0px; + margin-right: 25px; + margin-bottom: 25px; +} +a .show-seasons-card-overlay:hover { + webkit-box-shadow: inset 0 0 0 2px #e9a049; + -moz-box-shadow: inset 0 0 0 2px #e9a049; + box-shadow: inset 0 0 0 2px #e9a049; +} +.show-seasons-poster { + float: left; + position: relative; + left: 0px; +} +.show-seasons-card-overlay { + position: absolute; + left: 0; + right: 0; + bottom: 0; + text-align: left; + background: -moz-linear-gradient(top, rgba(0,0,0,0) 30%, rgba(0,0,0,1) 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(30%,rgba(0,0,0,0)), color-stop(100%,rgba(0,0,0,1))); + background: -webkit-linear-gradient(top, rgba(0,0,0,0) 30%,rgba(0,0,0,1) 100%); + background: -o-linear-gradient(top, rgba(0,0,0,0) 30%,rgba(0,0,0,1) 100%); + background: -ms-linear-gradient(top, rgba(0,0,0,0) 30%,rgba(0,0,0,1) 100%); + background: linear-gradient(to bottom, rgba(0,0,0,0) 30%,rgba(0,0,0,1) 100%); + height: 225px; +} +.show-seasons-overlay-text { + color: #aaa; + font-size: 12px; + float: left; + position: absolute; + left: 8px; + bottom: 5px; +} +.show-seasons-instance-text-wrapper { + width: 150px; + font-size: 13px; + margin-bottom: 20px; + clear: both; +} +.show-seasons-instance-text-wrapper h3 { + padding: 5px 3px 0 3px; + color: #fff; + text-overflow: ellipsis; + overflow: hidden; + position: relative; + font-size: 13px; + margin: 0; + line-height: 15px; + font-weight: normal; + width: 250px; + white-space: nowrap; + text-align: left; + clear: both; +} +.show-seasons-title a { + text-decoration: none; + font-size: 14px; + font-weight: normal; + color: #fff; + float: left; + text-overflow: ellipsis; + overflow: hidden; + position: relative; + white-space: nowrap; + width: 205px; + margin-top: 2px; + margin-left: 0px; + margin-bottom: 20px; +} +.show-seasons a:hover { + color: #F9AA03; } .season-episodes-wrapper { } .season-episodes-instance { list-style: none; - margin: 0 0 0px 0px; + margin: 0; } .season-episodes-instance li { float: left; position: relative; left: 0px; margin-right: 25px; + margin-bottom: 25px; } a .season-episodes-card-overlay:hover { - webkit-box-shadow: inset 0 0 0 2px #e9a049; + -webkit-box-shadow: inset 0 0 0 2px #e9a049; -moz-box-shadow: inset 0 0 0 2px #e9a049; box-shadow: inset 0 0 0 2px #e9a049; } @@ -1086,7 +1248,7 @@ a .season-episodes-card-overlay:hover { height: 140px; width: 250px; position: relative; - webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); + -webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); -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); } @@ -1097,7 +1259,7 @@ a .season-episodes-card-overlay:hover { height: 140px; } .season-episodes-poster-face img:hover { - webkit-box-shadow: 0 0 0 2px #F9AA03; + -webkit-box-shadow: 0 0 0 2px #F9AA03; -moz-box-shadow: 0 0 0 2px #F9AA03; box-shadow: 0 0 0 2px #F9AA03; } @@ -1120,11 +1282,13 @@ a .season-episodes-card-overlay:hover { font-size: 11px; text-shadow: 0 1px 5px rgba(0,0,0,0.2); } -.season-episodes-instance-text-wrapper { - width: 250px; - font-size: 13px; - margin-bottom: 20px; - clear: both; +.season-episodes-overlay-text { + color: #aaa; + font-size: 12px; + float: left; + position: absolute; + left: 8px; + bottom: 5px; } .season-episodes-instance-text-wrapper h3 { padding: 5px 3px 0 3px; @@ -1167,6 +1331,34 @@ a .season-episodes-card-overlay:hover { left: 8px; bottom: 5px; } +.settings-alert { + float: left; + padding: 0; + margin: 5px 0; + border: 0; + position: relative; +} +.settings-alert ul { + padding: 0; +} +.settings-alert ul li { + list-style: none; + padding: 5px 12px 5px 35px; + margin: 0; + border: 1px solid #ebccd1; + border-radius: 4px; +} +.settings-alert ul li:before { + content: "\f071"; + font-family: FontAwesome; + font-style: normal; + font-weight: normal; + text-decoration: inherit; + font-size: 18px; + position: absolute; + top: 5px; + left: 12px; +} .user-info-wrapper { height: 113px; } @@ -1180,22 +1372,8 @@ a .season-episodes-card-overlay:hover { -webkit-border-radius: 50%; -moz-border-radius: 50%; border-radius: 50%; - webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); - -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); -} -.user-info-poster-face img { - bottom: 0; - margin-right: 15px; - overflow: hidden; - float: left; - background-color: #323232; - background-position: center; - background-size: cover; - -webkit-border-radius: 1000px; - -moz-border-radius: 1000px; - border-radius: 1000px; -webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); + -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); } .user-info-username { @@ -1258,7 +1436,7 @@ a .season-episodes-card-overlay:hover { font-size: 12px; float: left; position: relative; - top: 14px; + top: 15px; left: 0px; } .user-overview-stats-instance h3 strong{ @@ -1305,7 +1483,7 @@ a .season-episodes-card-overlay:hover { float: left; width: 75px; border-radius: 3px; - webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); + -webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); -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); background-size: contain; @@ -1341,7 +1519,7 @@ a .season-episodes-card-overlay:hover { font-size: 12px; float: left; position: relative; - top: 14px; + top: 15px; left: 0px; } .home-platforms { @@ -1350,40 +1528,29 @@ a .season-episodes-card-overlay:hover { list-style: none; margin: 0; } -.home-platforms-instance-poster { - margin-left: 0px; +.home-platforms-instance { + background-color: #282828; + position: relative; + float: left; + width: 330px; + padding: 10px; + margin-right: 20px; + margin-bottom: 20px; + -webkit-box-sizing: content-box; + box-sizing: content-box; + -webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); + -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); } -.home-platforms-instance-poster .poster-face { +.home-platforms-instance li { + position: relative; +} +.home-platforms-instance-info { + float: left; + position: relative; + padding-left: 80px; + width: 100%; height: 120px; - width: 80px; -} -.home-platforms-instance-box { - background-size: contain; - position: absolute; - left: 10px; - bottom: 35px; - height: 80px; - width: 80px; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; - webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); - -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); -} -.home-platforms-instance-oval { - background-size: contain; - position: absolute; - left: 10px; - bottom: 35px; - height: 80px; - width: 80px; - -webkit-border-radius: 50%; - -moz-border-radius: 50%; - border-radius: 50%; - webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); - -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); } .home-platforms-instance-name { float: left; @@ -1395,64 +1562,345 @@ a .season-episodes-card-overlay:hover { line-height: 15px; font-weight: bold; width: 100%; - padding: 10px 0 0 10px; + padding: 0 0 0 20px; +} +.home-platforms-instance-name h4 { + margin: 10px 0 20px 0; +} +.home-platforms-instance-name h5 { + font-size: 14px; + line-height: 16px; + margin: 15px 0 2px 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.home-platforms-instance-name2 { + position: absolute; + top: 34px; + left: 215px; +} +.home-platforms-instance-name2 h5 { + font-size: 14px; + line-height: 16px; + margin: 15px 0 2px 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.home-platforms-instance-name2 h3 { + font-size: 30px; + font-weight: bold; + color: #F9AA03; + line-height: 22px; + position: relative; + top: 5px; + margin: 0 5px 0 0; + padding-top: 6px; + float: left; +} +.home-platforms-instance-name2 p { + color: #aaa; + font-size: 12px; + float: left; + position: relative; + top: 21px; + left: 0px; } .home-platforms-instance-playcount { float: left; - width: 180px; -} -.home-platforms-instance-mediainfo { - float: left; - background-color: #282828; - position: absolute; - bottom: 0; - left: 0; - padding-left: 170px; - width: 100%; - height: 175px; -} -.home-platforms-instance-media { position: relative; - float: left; - width: 375px; - height: 225px; - padding-bottom: 10px; - margin-right: 25px; - margin-bottom: 25px; - webkit-box-sizing: content-box; - box-sizing: content-box; + padding: 6px 0 0 20px; + width: 100%; } -.home-platforms-instance-info { - float: left; - background-color: #282828; - position: absolute; +.home-platforms-instance-playcount h3 { + font-size: 30px; + font-weight: bold; + color: #F9AA03; + line-height: 22px; + position: relative; top: 5px; - left: 0; - padding-left: 100px; - width: 100%; - height: 120px; -} -.home-platforms-instance { - background-color: #282828; - position: relative; + margin: 0 5px 0 0; float: left; - width: 300px; +} +.home-platforms-instance-playcount p { + color: #aaa; + font-size: 12px; + float: left; + position: relative; + top: 15px; + left: 0px; + margin-right: 5px; +} +.home-platforms-instance-last-user { + float: left; + position: relative; + padding: 6px 0 0 20px; + width: 100%; +} +.home-platforms-instance-last-user h5 { + font-size: 12px; + position: relative; + margin: 0 0 2px 0; + float: left; +} +.home-platforms-instance-last-user p { + color: #aaa; + font-size: 12px; + float: left; + clear: left; + position: relative; + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.home-platforms-instance-poster { + margin-left: 0px; + position: absolute; +} +.home-platforms-instance-poster .home-platforms-poster-face { + background-position: center; + background-size: cover; height: 120px; - padding: 10px; - margin-right: 20px; - margin-bottom: 20px; - webkit-box-sizing: content-box; - box-sizing: content-box; + width: 80px; + -webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); + -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); } -a .home-platforms-instance-oval:hover { - webkit-box-shadow: inset 0 0 0 2px #e9a049; +.home-platforms-instance-poster .home-platforms-library-thumb { + background-position: center; + background-size: cover; + height: 80px; + width: 80px; + margin-top: 20px; +} +.home-platforms-instance-box { + background-position: center; + background-size: cover; + margin: 20px 0 0 0px; + height: 80px; + width: 80px; + position: relative; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + -webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); + -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); + overflow: hidden; +} +.home-platforms-instance-oval { + background-position: center; + background-size: cover; + margin: 20px 0 0 0px; + height: 80px; + width: 80px; + position: relative; + -webkit-border-radius: 50%; + -moz-border-radius: 50%; + border-radius: 50%; + -webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); + -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); + overflow: hidden; +} +.home-platforms-instance-list { + float: left; + position: relative; + width: 100%; + padding: 0 10px 25px 10px; +} +.home-platforms-instance-list li { + margin-top: 25px; + position: relative; + height: 60px; +} +.home-platforms-instance-list-number { + background-color: #eb8600; + float: left; + position: absolute; + top: -10px; + left: 10px; + height: 20px; + width: 20px; + display: block; + text-align: center; + padding-top: 1px; + -webkit-border-radius: 50%; + -moz-border-radius: 50%; + border-radius: 50%; + -webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); + -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); +} +.home-platforms-instance-list-number h4 { + color: #000; + font-size: 15px; + font-weight: bold; + margin: 0; +} +.home-platforms-instance-list-info { + float: left; + position: relative; + padding-left: 75px; + width: 100%; + height: 60px; +} +.home-platforms-instance-list-name { + float: left; + color: #fff; + text-overflow: ellipsis; + overflow: hidden; + position: relative; + font-size: 13px; + line-height: 15px; + font-weight: bold; + width: 100%; + padding: 2px 0 0 10px; +} +.home-platforms-instance-list-name h5 { + margin: 5px 0px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.home-platforms-instance-list-playcount { + float: left; + position: relative; + padding: 4px 0 0 10px; + width: 100%; +} +.home-platforms-instance-list-playcount h3 { + font-size: 20px; + font-weight: bold; + color: #F9AA03; + line-height: 22px; + position: relative; + margin: 0 5px 0 0; + float: left; +} +.home-platforms-instance-list-playcount p { + color: #aaa; + font-size: 12px; + float: left; + position: relative; + top: 5px; + left: 0px; + margin-right: 5px; +} +.home-platforms-instance-list-last-user { + float: left; + position: relative; + padding: 0 0 0 10px; + width: 100%; +} +.home-platforms-instance-list-last-user h5 { + font-size: 12px; + position: relative; + margin: 0 0 2px 0; + float: left; +} +.home-platforms-instance-list-last-user p { + color: #aaa; + font-size: 12px; + float: left; + clear: left; + position: relative; + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.home-platforms-instance-list-poster { + position: absolute; + left: 20px; +} +.home-platforms-instance-list-poster .home-platforms-list-poster-face { + background-position: center; + background-size: cover; + height: 60px; + width: 40px; + -webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); + -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); +} +.home-platforms-instance-list-box { + background-position: center; + background-size: cover; + margin: 10px 0 0 20px; + height: 40px; + width: 40px; + position: relative; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + -webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); + -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); + overflow: hidden; +} +.home-platforms-instance-list-oval { + background-position: center; + background-size: cover; + margin: 10px 0 0 20px; + height: 40px; + width: 40px; + position: relative; + -webkit-border-radius: 50%; + -moz-border-radius: 50%; + border-radius: 50%; + -webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); + -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); + overflow: hidden; +} +.home-platforms-instance-list-chevron { + position: absolute; + top: 105px; + right: 0; + cursor: pointer; +} +.home-platforms-instance-list-chevron i { + color: #999; + font-size: 16px; + -webkit-transition: all 0.3s ease; + -o-transition: all 0.3s ease; + transition: all 0.3s ease; +} +.home-platforms-instance-list-chevron i:hover { + color: #eb8600; +} +.home-platforms-instance-list-chevron.active i.fa-chevron-down{ + color: #eb8600; + -webkit-transform: rotate(180deg); + -ms-transform: rotate(180deg); + -o-transform: rotate(180deg); + transform: rotate(180deg); +} +a .home-platforms-instance-box:hover, +a .home-platforms-instance-oval:hover, +a .home-platforms-instance-list-box:hover, +a .home-platforms-instance-list-oval:hover, +.home-platforms-poster-face:hover, +.home-platforms-list-poster-face:hover + { + -webkit-box-shadow: inset 0 0 0 2px #e9a049; -moz-box-shadow: inset 0 0 0 2px #e9a049; box-shadow: inset 0 0 0 2px #e9a049; } -a .home-platforms-instance-box:hover { - webkit-box-shadow: inset 0 0 0 2px #e9a049; - -moz-box-shadow: inset 0 0 0 2px #e9a049; - box-shadow: inset 0 0 0 2px #e9a049; +.home-platforms-instance .slider { + background-color: #2d2d2d; + width: calc(100% + 20px); + display: none; + position: absolute; + top: 128px; + left: -10px; + -webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); + -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); + z-index: 1; + clip: rect(1px, 354px, 1000px, -2px); } .history-table-title { text-overflow: ellipsis; @@ -1528,7 +1976,8 @@ a .home-platforms-instance-box:hover { .button-bar { float: right; } -.colvis-button-bar { +.colvis-button-bar, +.refresh-users-button { float: right; } .nav-settings, @@ -1747,6 +2196,7 @@ a .home-platforms-instance-box:hover { } .history-title .popover.right { margin-left: 12px; + z-index: 5; } .history-title .popover.right .popover-content { padding: 5px 8px; @@ -1783,6 +2233,34 @@ a .home-platforms-instance-box:hover { -o-transition: none !important; transition: none !important; } +.settings-alert { + float: left; + padding: 0; + margin: 5px 0; + border: 0; + position: relative; +} +.settings-alert ul { + padding: 0; +} +.settings-alert ul li { + list-style: none; + padding: 5px 12px 5px 35px; + margin: 0; + border: 1px solid #ebccd1; + border-radius: 4px; +} +.settings-alert ul li:before { + content: "\f071"; + font-family: FontAwesome; + font-style: normal; + font-weight: normal; + text-decoration: inherit; + font-size: 18px; + position: absolute; + top: 5px; + left: 12px; +} #users-to-delete > li { color: #e9a049; } @@ -1824,7 +2302,40 @@ a .home-platforms-instance-box:hover { position: relative; margin-right: 3px; } - #updatebar a:hover { color: #F9AA03; +} +.body-container { + position: absolute; + top: 50px; + right: 0; + bottom: 0; + left: 0; + overflow-x: hidden; + overflow-y: auto; + +} +::-webkit-scrollbar { + width: 15px; +} +::-webkit-scrollbar-track { + background-color: rgba(0,0,0,.2); +} +::-webkit-scrollbar-thumb { + min-height: 50px; + background-color: rgba(255,255,255,.15); + border: 3px solid transparent; + border-radius: 8px; + background-clip: padding-box; +} + +@media only screen + and (min-device-width: 300px) + and (max-device-width: 400px) { + .home-platforms-instance { + width: calc(100% - 20px); + } + .dashboard-instance { + width: 100%; + } } \ No newline at end of file diff --git a/data/interfaces/default/current_activity.html b/data/interfaces/default/current_activity.html index 3bea2b89..d166683d 100644 --- a/data/interfaces/default/current_activity.html +++ b/data/interfaces/default/current_activity.html @@ -28,8 +28,10 @@ user Returns the name of the user owning the session. user_id Returns the Plex user id if available. machine_id Returns the machine id of the players being used. friendly_name Returns the friendlly name of the user owning the session. +user_thumb Returns the profile picture of the user owning the session. state Returns the state of the current session. Either 'playing', 'paused' or 'buffering'. title Returns the name of the episode, movie or music track. +year Returns the year of the episode, movie, or clip. player Returns the name of the platform used to play the stream. platform Returns the type of platform used to play the stream. audio_decision Returns the audio transcode decision. Either 'transcode', 'copy' or 'direct play'. @@ -61,159 +63,166 @@ DOCUMENTATION :: END % if data is not None: % if data['stream_count'] != '0': % for a in data['sessions']: -
-
-
+
+ % if a['type'] == 'movie' or a['type'] == 'episode': + + % endif +
+ % if a['type'] == 'movie' and not a['indexes']: - +
+ % elif a['type'] == 'episode' and not a['indexes']: +
% elif a['indexes']: - + % else: - % if a['type'] == 'track': -
- % endif - % if a['type'] == 'clip': - - % else: - - % endif - % endif -
-
- % if a['type'] == 'track': - Track ${a['media_index']} - % elif a['type'] == 'episode': - Season ${a['parent_media_index']}, Episode ${a['media_index']} + % if a['type'] == 'track': +
+
+ % elif a['type'] == 'clip': + % if a['art'][:4] == 'http': +
+ % elif a['thumb'][:4] == 'http': +
% else: - ${a['title']} + % if a['art']: +
+ % else: +
+ % endif % endif + % else: +
+ % endif + % endif +
+ +
+
+
+
+ ${a['player']}
+ % if a['state'] == 'playing': + State  Playing + % elif a['state'] == 'paused': + State  Paused + % elif a['state'] == 'buffering': + State  Buffering + % endif +
+ % if a['type'] == 'track': + % if a['audio_decision'] == 'direct play': + Stream  Direct Play + % else: + Stream  Transcoding + % endif +
+ % if a['audio_decision'] == 'direct play': + Audio  Direct Play (${a['audio_codec']}) (${a['audio_channels']}ch) + % elif a['audio_decision'] == 'Copy': + Audio  Direct Stream (${a['transcode_audio_codec']}) (${a['transcode_audio_channels']}ch) + % elif a['audio_decision'] != 'transcode': + Audio  Transcode (${a['transcode_audio_codec']}) (${a['transcode_audio_channels']}ch) + % endif + % elif a['type'] == 'episode' or a['type'] == 'movie' or a['type'] == 'clip': + % if a['video_decision'] == 'direct play': + Stream  Direct Play + % else: + Stream  Transcoding + % endif +
+ % if a['video_decision'] == 'direct play': + Video  Direct Play (${a['video_codec']}) (${a['width']}x${a['height']}) + % elif a['video_decision'] == 'copy': + Video  Direct Stream (${a['transcode_video_codec']}) (${a['width']}x${a['height']}) + % elif a['video_decision'] == 'transcode': + Video  Transcode (${a['transcode_video_codec']}) (${a['transcode_width']}x${a['transcode_height']}) + % endif +
+ % if a['audio_decision'] == 'direct play': + Audio  Direct Play (${a['audio_codec']}) (${a['audio_channels']}ch) + % elif a['audio_decision'] == 'copy': + Audio  Direct Stream (${a['transcode_audio_codec']}) (${a['transcode_audio_channels']}ch) + % elif a['audio_decision'] == 'transcode': + Audio  Transcode (${a['transcode_audio_codec']}) (${a['transcode_audio_channels']}ch) + % endif + % endif +
+
+
${a['progress']}/${a['duration']}
-
% endfor % else: @@ -223,4 +232,4 @@ DOCUMENTATION :: END
There was an error communicating with your Plex Server. Please check your settings.

-% endif \ No newline at end of file +% endif diff --git a/data/interfaces/default/history.html b/data/interfaces/default/history.html index c7960d20..0615ea28 100644 --- a/data/interfaces/default/history.html +++ b/data/interfaces/default/history.html @@ -103,7 +103,7 @@ data: { row_id: history_to_delete[i] }, async: true, success: function (data) { - var msg = "User history purged"; + var msg = "History deleted"; showMsg(msg, false, true, 2000); } }); @@ -113,7 +113,6 @@ } $('.delete-control').each(function () { - $(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger'); $(this).addClass('hidden'); $('#row-edit-mode-alert').fadeOut(200); }); @@ -121,6 +120,7 @@ } else { history_to_delete = []; $('.delete-control').each(function() { + $(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger'); $(this).removeClass('hidden'); }); } diff --git a/data/interfaces/default/history_table_modal.html b/data/interfaces/default/history_table_modal.html index cfff340d..152589eb 100644 --- a/data/interfaces/default/history_table_modal.html +++ b/data/interfaces/default/history_table_modal.html @@ -27,7 +27,8 @@
- + % else: diff --git a/data/interfaces/default/home_stats.html b/data/interfaces/default/home_stats.html index 2a05ca34..175b3887 100644 --- a/data/interfaces/default/home_stats.html +++ b/data/interfaces/default/home_stats.html @@ -17,6 +17,9 @@ data[array_index]['rows'] :: Usable parameters row_id Return the db row id for a metadata item if one exists +== Only if 'stat_id' is 'top_tv' or 'popular_tv' or 'top_movies' or 'popular_movies' or 'last_watched' == +thumb Return the thumb for the media item. + == Only if 'stat_id' is 'top_tv' or 'popular_tv' == grandparent_thumb Returns location of the item's thumbnail. Use with pms_image_proxy. rating_key Returns the unique identifier for the media item. @@ -29,15 +32,18 @@ total_duration Returns the total duration for the associated stat. == Only of 'stat_id' is 'popular_tv' or 'popular_movies' == users_watched Returns the count for the associated stat. -== Only if 'stat_id' is 'top_user' == -thumb Returns url of the user's gravatar. Returns '' if none exists. +== Only if 'stat_id' is 'top_user' or 'last_watched' == +user_thumb Returns url of the user's gravatar. Returns '' if none exists. user Returns the username for the associated stat. user_id Returns the user id for the associated stat. friendly_name Returns the friendly name of the user for the associated stat. -== Only if 'stat_id' is 'top_platform' == +== Only if 'stat_id' is 'top_platform' or 'last_watched' == platform_type Returns the platform name for the associated stat. +== Only if 'stat_id' is 'last_watched' == +last_watch Returns the time the media item was last watched. + DOCUMENTATION :: END @@ -48,7 +54,7 @@ DOCUMENTATION :: END def hd(minutes): if int(minutes) > 60: hours = int(helpers.cast_to_float(minutes) / 60) - minutes = int(helpers.cast_to_float(minutes) % hours) + minutes = int(helpers.cast_to_float(minutes) % 60 ) if minutes > 0: return "

" + str(hours) + "

hrs

" + str(minutes) + "

mins

" else: @@ -58,199 +64,587 @@ DOCUMENTATION :: END %> % if data: -% if data[0]['rows'] or data[2]['rows']: +% if data[0]['rows'] or data[1]['rows'] or data[2]['rows'] or data[3]['rows'] or data[4]['rows'] or data[5]['rows']: + % else:
No stats for selected period.

% endif diff --git a/data/interfaces/default/index.html b/data/interfaces/default/index.html index 9e221a9d..b73bdbe0 100644 --- a/data/interfaces/default/index.html +++ b/data/interfaces/default/index.html @@ -19,7 +19,7 @@
-

Statistics Last ${config['home_stats_length']} days

+

Watch Statistics Last ${config['home_stats_length']} days

Loading stats...
@@ -27,6 +27,17 @@
+
+
+
+

Library Statistics

+
+
+
Loading stats...
+
+
+
+
@@ -45,17 +56,18 @@ diff --git a/data/interfaces/default/info.html b/data/interfaces/default/info.html index d52d19aa..08c5005b 100644 --- a/data/interfaces/default/info.html +++ b/data/interfaces/default/info.html @@ -47,201 +47,220 @@ DOCUMENTATION :: END % if data:
-
-
-
-
-
-
- -
-
- % if data['type'] == 'movie': -

${data['title']}

- % elif data['type'] == 'season': -

${data['parent_title']} (${data['title']})

- % elif data['type'] == 'episode': -

${data['grandparent_title']} - ${data['title']} - (Season ${data['parent_index']}, Episode ${data['index']})

- % else: -

${data['title']}

- % endif -
- % if (data['type'] == 'movie' or data['type'] == 'show' or data['type'] == 'episode') and data['rating']: - - % endif -
-
- % if (data['type'] == 'episode' or data['type'] == 'movie') and data['directors']: - Directed by ${data['directors'][0]} - % endif -
-
- % if (data['type'] == 'show' or data['type'] == 'movie') and data['studio']: - Studio ${data['studio']} - % endif -
-
- % if data['type'] == 'movie': - Year ${data['year']} - % elif data['type'] == 'show': - Aired ${data['year']} - % elif data['type'] == 'episode': - Aired ${data['originally_available_at']} - % endif -
-
- Runtime ${data['duration']} mins -
-
- % if (data['type'] == 'episode' or data['type'] == 'movie' or data['type'] == 'show') and data['content_rating']: - Rated ${data['content_rating']} - % endif -
-
-
-

${data['summary']}

-
-
-
-
- -
-
+
+
+
+
+
+ % if data['type'] == 'movie': + Movies + + ${data['title']} + % elif data['type'] == 'show': + TV Shows + + ${data['title']} + % elif data['type'] == 'season': + TV Shows + + ${data['parent_title']} + + Season ${data['index']} + % elif data['type'] == 'episode': + TV Shows + + ${data['grandparent_title']} + + Season ${data['parent_index']} + + Episode ${data['index']} - ${data['title']} + % endif
-
-
-
-% if data['type'] == 'movie' or data['type'] == 'episode' or data['type'] == 'show': -
-
-
-
-
- Watch History for ${data['title']} -
-
- -   - - -
-
-
- - - - - - - - - - - - - - - - - - -
DeleteTimeUserIP AddressPlatformTitleStartedPausedStoppedDuration
-
- - - -% elif data['type'] == 'season': -
-
-
-
-
- Episode List for ${data['title']} +
+
+ % if data['type'] == 'movie' or data['type'] == 'show' or data['type'] == 'season': + + % else: + + % endif +
+
+ % if (data['type'] == 'movie' or data['type'] == 'show' or data['type'] == 'episode') and data['rating']: + + % endif +
+ % if (data['type'] == 'episode' or data['type'] == 'movie') and data['directors']: + Directed by ${data['directors'][0]} + % endif +
+
+ % if (data['type'] == 'show' or data['type'] == 'movie') and data['studio']: + Studio ${data['studio']} + % endif +
+
+ % if data['type'] == 'movie': + Year ${data['year']} + % elif data['type'] == 'show': + Aired ${data['year']} + % elif data['type'] == 'episode': + Aired ${data['originally_available_at']} + % endif +
+
+ Runtime ${data['duration']} mins +
+
+ % if (data['type'] == 'episode' or data['type'] == 'movie' or data['type'] == 'show') and data['content_rating']: + Rated ${data['content_rating']} + % endif +
+
+
+

${data['summary']}

+
+
+
+ + +
+ % if data['type'] == 'show': +
+
+
+ Season List for ${data['title']} +
+
+
+
+
+
+ % elif data['type'] == 'season': +
+
+
+ Episode List for ${data['title']} +
+
+
+
+
+
+ % endif + % if data['type'] == 'movie' or data['type'] == 'episode' or data['type'] == 'show' or data['type'] == 'season': +
+
+
+ Watch History for ${data['title']} +
+
+ + + +
+
+
+ + + + + + + + + + + + + + + + + +
DeleteTimeUserIP AddressPlatformTitleStartedPausedStoppedDuration
+
+ + +
+ % endif
-
-
-
- % endif
@@ -249,8 +268,10 @@ DOCUMENTATION :: END
-

Error retrieving item data. This media may not be available in the Plex Media Server database - anymore.

+

+ Error retrieving item data. This media may not be available in the Plex Media Server database + anymore. +

@@ -282,70 +303,13 @@ DOCUMENTATION :: END type: 'post', data: function ( d ) { return { 'json_data': JSON.stringify( d ), - 'rating_key': ${data['rating_key']} - }; + 'rating_key': ${data['rating_key']} }; } } history_table = $('#history_table').DataTable(history_table_options); var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: ' Select columns', buttonClass: 'btn btn-dark', exclude: [0, 10] }); $(colvis.button()).appendTo('div.colvis-button-bar'); - - clearSearchButton('history_table', history_table); - - $('#row-edit-mode').on('click', function() { - $('#delete-message').popover(); - - if ($(this).hasClass('active')) { - if (history_to_delete.length > 0) { - $('#deleteCount').text(history_to_delete.length); - $('#confirm-modal').modal(); - $('#confirm-modal').one('click', '#confirm-delete', function () { - for (var i = 0; i < history_to_delete.length; i++) { - $.ajax({ - url: 'delete_history_rows', - data: { row_id: history_to_delete[i] }, - async: true, - success: function (data) { - var msg = "User history purged"; - showMsg(msg, false, true, 2000); - } - }); - } - history_table.draw(); - }); - } - - $('.delete-control').each(function () { - $(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger'); - $(this).addClass('hidden'); - }); - - } else { - history_to_delete = []; - $('.delete-control').each(function() { - $(this).removeClass('hidden'); - }); - } - }); - }); - -% elif data['type'] == 'show': - - -% endif - -% if data['type'] == 'season': +% elif data['type'] == 'show': + +% endif +% if data['type'] == 'season': + + % endif % endif + diff --git a/data/interfaces/default/info_episode_list.html b/data/interfaces/default/info_episode_list.html index ef02c42c..11651969 100644 --- a/data/interfaces/default/info_episode_list.html +++ b/data/interfaces/default/info_episode_list.html @@ -34,7 +34,7 @@ DOCUMENTATION :: END
-
+
Episode ${a['index']}
diff --git a/data/interfaces/default/info_season_list.html b/data/interfaces/default/info_season_list.html new file mode 100644 index 00000000..6da68e0e --- /dev/null +++ b/data/interfaces/default/info_season_list.html @@ -0,0 +1,55 @@ +<%doc> +USAGE DOCUMENTATION :: PLEASE LEAVE THIS AT THE TOP OF THIS FILE + +For Mako templating syntax documentation please visit: http://docs.makotemplates.org/en/latest/ + +Filename: info_season_list.html +Version: 0.1 +Variable names: data [list] + +data :: Usable parameters + +== Global keys == +season_count Returns the number of seasons in the array. +season_list Returns an array of seasons. + +data['season_list'] :: Usable paramaters + +== Global keys == +rating_key Returns the unique identifier for the media item. +thumb Returns the location of the item's thumbnail. Use with pms_image_proxy. +title Returns the name of the season. +index Returns the season number. + +DOCUMENTATION :: END + + +% if data != None: +% if data['season_count'] > 0: +
+ +
+% endif +% endif \ No newline at end of file diff --git a/data/interfaces/default/js/tables/history_table_modal.js b/data/interfaces/default/js/tables/history_table_modal.js index 1c684907..8a0afa22 100644 --- a/data/interfaces/default/js/tables/history_table_modal.js +++ b/data/interfaces/default/js/tables/history_table_modal.js @@ -74,6 +74,19 @@ history_table_modal_options = { { "targets": [3], "data":"player", + "createdCell": function (td, cellData, rowData, row, col) { + if (cellData !== '') { + var transcode_dec = ''; + if (rowData['video_decision'] === 'transcode') { + transcode_dec = ''; + } else if (rowData['video_decision'] === 'copy') { + transcode_dec = ''; + } else if (rowData['video_decision'] === 'direct play' || rowData['video_decision'] === '') { + transcode_dec = ''; + } + $(td).html(''); + } + }, "className": "no-wrap hidden-sm hidden-xs modal-control" }, { @@ -81,14 +94,21 @@ history_table_modal_options = { "data":"full_title", "createdCell": function (td, cellData, rowData, row, col) { if (cellData !== '') { - if (rowData['media_type'] === 'movie' || rowData['media_type'] === 'episode') { - var transcode_dec = ''; - if (rowData['video_decision'] === 'transcode') { - transcode_dec = ' '; - } - $(td).html('
' + transcode_dec + '
'); + var media_type = ''; + var thumb_popover = ''; + if (rowData['media_type'] === 'movie') { + media_type = ''; + thumb_popover = '' + cellData + ' (' + rowData['year'] + ')' + $(td).html(''); + } else if (rowData['media_type'] === 'episode') { + media_type = ''; + thumb_popover = '' + cellData + ' \ + (S' + ('00' + rowData['parent_media_index']).slice(-2) + 'E' + ('00' + rowData['media_index']).slice(-2) + ')' + $(td).html(''); } else if (rowData['media_type'] === 'track') { - $(td).html('
' + cellData + '
'); + media_type = ''; + thumb_popover = '' + cellData + ' (' + rowData['parent_title'] + ')' + $(td).html('
' + media_type + ' ' + thumb_popover + '
'); } else { $(td).html('' + cellData + ''); } @@ -100,9 +120,40 @@ history_table_modal_options = { // Jump to top of page // $('html,body').scrollTop(0); $('#ajaxMsg').fadeOut(); + + // Create the tooltips. + $('.transcode-tooltip').tooltip(); + $('.media-type-tooltip').tooltip(); + $('.thumb-tooltip').popover({ + html: true, + trigger: 'hover', + placement: 'right', + content: function () { + return '
'; + } + }); }, "preDrawCallback": function(settings) { var msg = "
 Fetching rows...
"; showMsg(msg, false, false, 0) } -} \ No newline at end of file +} + +$('#history_table').on('click', 'td.modal-control', function () { + var tr = $(this).parents('tr'); + var row = history_table.row(tr); + var rowData = row.data(); + + function showStreamDetails() { + $.ajax({ + url: 'get_stream_data', + data: { row_id: rowData['id'], user: rowData['friendly_name'] }, + cache: false, + async: true, + complete: function (xhr, status) { + $("#info-modal").html(xhr.responseText); + } + }); + } + showStreamDetails(); +}); \ No newline at end of file diff --git a/data/interfaces/default/js/tables/users.js b/data/interfaces/default/js/tables/users.js index 8a5154df..0e9719db 100644 --- a/data/interfaces/default/js/tables/users.js +++ b/data/interfaces/default/js/tables/users.js @@ -1,3 +1,5 @@ +var users_to_purge = []; + users_list_table_options = { "language": { "search": "Search: ", @@ -187,6 +189,11 @@ users_list_table_options = { "preDrawCallback": function(settings) { var msg = "
 Fetching rows...
"; showMsg(msg, false, false, 0) + }, + "rowCallback": function (row, rowData) { + if ($.inArray(rowData['user_id'], users_to_purge) !== -1) { + $(row).find('button[data-id="' + rowData['user_id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger'); + } } } @@ -271,9 +278,11 @@ $('#users_list_table').on('click', 'td.edit-control > .edit-user-toggles > butto var row = users_list_table.row(tr); var rowData = row.data(); - if ($(this).hasClass('active')) { - $(this).toggleClass('btn-warning').toggleClass('btn-danger'); + var index = $.inArray(rowData['user_id'], users_to_purge); + if (index === -1) { + users_to_purge.push(rowData['user_id']); } else { - $(this).toggleClass('btn-danger').toggleClass('btn-warning'); + users_to_purge.splice(index, 1); } + $(this).toggleClass('btn-warning').toggleClass('btn-danger'); }); \ No newline at end of file diff --git a/data/interfaces/default/library_stats.html b/data/interfaces/default/library_stats.html new file mode 100644 index 00000000..66b774d9 --- /dev/null +++ b/data/interfaces/default/library_stats.html @@ -0,0 +1,78 @@ +<%doc> +USAGE DOCUMENTATION :: PLEASE LEAVE THIS AT THE TOP OF THIS FILE + +For Mako templating syntax documentation please visit: http://docs.makotemplates.org/en/latest/ + +Filename: library_stats.html +Version: 0.1 +Variable names: data [array] + +data[array_index] :: Usable parameters + +data['type'] Returns the type of the library. Either 'movie', 'show', 'photo', or 'artist'. +data['rows'] Returns an array containing stat data + +data[array_index]['rows'] :: Usable parameters + +title Returns the title of the library. +thumb Returns the thumb of the library. +count Returns the number of items in the library. +count_type Returns the sorting type for the library + +== Only if 'type' is 'show' +episode_count Return the number of episodes in the library. +episode_count_type Return the sorting type for the episodes. + +== Only if 'type' is 'artist' +album_count Return the number of episodes in the library. +album_count_type Return the sorting type for the episodes. + +DOCUMENTATION :: END + + +% if data: +
    + % for library in data: +
    +
  • +
    +
    +

    ${library['rows']['title']}

    +
    ${library['rows']['count_type']}
    +
    +
    +

    ${library['rows']['count']}

    +

    items

    +
    + % if library['type'] == 'show': +
    +
    ${library['rows']['episode_count_type']}
    +

    ${library['rows']['episode_count']}

    +

    items

    +
    + % endif + % if library['type'] == 'artist': +
    +
    ${library['rows']['album_count_type']}
    +

    ${library['rows']['album_count']}

    +

    items

    +
    + % endif +
    + % if library['rows']['thumb']: +
    +
    +
    + % else: +
    +
    +
    + % endif +
  • +
    + % endfor +
+% else: +
Unable to retrieve data from database. Please check your settings. +

+% endif \ No newline at end of file diff --git a/data/interfaces/default/settings.html b/data/interfaces/default/settings.html index 3145c4ae..797097a2 100644 --- a/data/interfaces/default/settings.html +++ b/data/interfaces/default/settings.html @@ -91,11 +91,22 @@ available_notification_agents = notifiers.available_notification_agents()
- +
+

Specify the number of days for the statistics on the home page. Default is 30 days.

+
+ +
+
+ +
+ +
+

Specify the number of items to show in the top lists for the statistics on the home page. Max is 10 items, default is 5 items, 0 to disable.

+
@@ -223,8 +235,9 @@ available_notification_agents = notifiers.available_notification_agents()
- +
+

Port that Plex Media Server is listening on.

@@ -286,8 +299,9 @@ available_notification_agents = notifiers.available_notification_agents()
- +
+

The interval (in hours) PlexPy will request an updated friends list from Plex.tv. 1 minimum, 24 maximum.

@@ -328,10 +342,11 @@ available_notification_agents = notifiers.available_notification_agents()
- +
+
-

The interval (in seconds) PlexPy will ping your Plex Server. Min 30 seconds, Recommended 60 seconds.

+

The interval (in seconds) PlexPy will ping your Plex Server. Min 30 seconds, recommended 60 seconds.

@@ -344,11 +359,12 @@ available_notification_agents = notifiers.available_notification_agents()

Keep records of all video items played from your Plex Media Server.

- +
- +
+

The interval (in seconds) PlexPy will wait for a video item to be active before logging it. 0 to disable.

@@ -380,8 +396,9 @@ available_notification_agents = notifiers.available_notification_agents()
- +
+

How many buffer events should we wait before triggering the first warning. Buffer events increment on each monitor ping if play state is buffering. 0 to disable buffer warnings.

@@ -389,8 +406,9 @@ available_notification_agents = notifiers.available_notification_agents()
- +
+

The value (in seconds) PlexPy should wait before triggering the next buffer warning. 0 to always trigger.

@@ -421,8 +439,9 @@ available_notification_agents = notifiers.available_notification_agents()
- +
+

Set the progress percentage of when a watched notification should be triggered. Minimum 50, Maximum 95.

@@ -552,7 +571,7 @@ available_notification_agents = notifiers.available_notification_agents()

Notification Agents

- Toggle the desired notification options by clicking the bolt icon and configure it by selecting the settings icon to the right. + Toggle the desired notification options by clicking the bell icon and configure it by clicking the settings icon to the right.


    @@ -560,9 +579,9 @@ available_notification_agents = notifiers.available_notification_agents()
  • % if agent['on_play'] or agent['on_stop'] or agent['on_pause'] or agent['on_resume'] or agent['on_buffer'] or agent['on_watched']: - + % else: - + % endif ${agent['name']} % if agent['has_config']: @@ -1160,4 +1179,4 @@ $(document).ready(function() { var accordion = new Accordion($('#accordion'), false); }); - \ No newline at end of file + diff --git a/data/interfaces/default/stream_data.html b/data/interfaces/default/stream_data.html index 6aba69ec..c0cf86fd 100644 --- a/data/interfaces/default/stream_data.html +++ b/data/interfaces/default/stream_data.html @@ -54,6 +54,7 @@ DOCUMENTATION :: END

    Stream Details

    + % if data['media_type'] != 'track':
    Video
      % if data['transcode_video_dec'] != 'direct play': @@ -74,6 +75,7 @@ DOCUMENTATION :: END
    • Video Height: ${data['height']}
    • % endif
    + % endif
    Audio
      % if data['transcode_audio_dec'] != 'direct play': @@ -91,11 +93,14 @@ DOCUMENTATION :: END

      Media Source Details

      • Container: ${data['container']}
      • + % if data['media_type'] != 'track':
      • Resolution: ${data['height']}p
      • + % endif
      • Bitrate: ${data['bitrate']} kbps
    + % if data['media_type'] != 'track':

    Video Source Details

    • Width: ${data['width']}
    • @@ -104,6 +109,7 @@ DOCUMENTATION :: END
    • Video Frame Rate: ${data['video_framerate']}
    • Video Codec: ${data['video_codec']}
    + % endif

    Audio Source Details

    • Audio Codec: ${data['audio_codec']}
    • diff --git a/data/interfaces/default/user.html b/data/interfaces/default/user.html index c2176a10..fa90d112 100644 --- a/data/interfaces/default/user.html +++ b/data/interfaces/default/user.html @@ -154,13 +154,11 @@ from plexpy import helpers
    - -   - - + + +
    @@ -170,8 +168,8 @@ from plexpy import helpers Delete Time User - Platform IP Address + Platform Title Started Paused @@ -390,7 +388,7 @@ from plexpy import helpers }); $('#row-edit-mode').on('click', function() { - $('#delete-message').popover(); + $('#row-edit-mode-alert').fadeIn(200); if ($(this).hasClass('active')) { if (history_to_delete.length > 0) { @@ -403,7 +401,7 @@ from plexpy import helpers data: { row_id: history_to_delete[i] }, async: true, success: function (data) { - var msg = "User history purged"; + var msg = "History deleted"; showMsg(msg, false, true, 2000); } }); @@ -413,13 +411,14 @@ from plexpy import helpers } $('.delete-control').each(function () { - $(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger'); $(this).addClass('hidden'); + $('#row-edit-mode-alert').fadeOut(200); }); } else { history_to_delete = []; $('.delete-control').each(function() { + $(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger'); $(this).removeClass('hidden'); }); } diff --git a/data/interfaces/default/users.html b/data/interfaces/default/users.html index df36a858..fe35f072 100644 --- a/data/interfaces/default/users.html +++ b/data/interfaces/default/users.html @@ -12,12 +12,11 @@ All Users
    - -   - - + +   +
@@ -85,20 +84,19 @@ clearSearchButton('users_list_table', users_list_table); - var users_to_purge = []; $('#row-edit-mode').on('click', function () { - $('#purge-message').popover(); + $('#row-edit-mode-alert').fadeIn(200); + $('#users-to-delete').html(''); if ($(this).hasClass('active')) { - users_to_purge = []; - ul = $('#users-to-delete'); - ul.html(''); - $('.edit-control button.btn-danger').map(function () { - users_to_purge.push($(this).attr('data-id')); - ul.append('
  • ' + $('div[data-id=' + $(this).attr('data-id') + '] > input').val() + '
  • ') - }); if (users_to_purge.length > 0) { - $('#users-to-delete').append + $('.edit-control').each(function () { + $(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger'); + }); + + for (var i = 0; i < users_to_purge.length; i++) { + $('#users-to-delete').append('
  • ' + $('div[data-id=' + users_to_purge[i] + '] > input').val() + '
  • '); + } $('#confirm-modal').modal(); $('#confirm-modal').one('click', '#confirm-purge', function () { for (var i = 0; i < users_to_purge.length; i++) { @@ -118,35 +116,16 @@ } $('.edit-control').each(function () { - $(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger'); $(this).addClass('hidden'); - }); - $('.edit-user-control > .edit-user-name').each(function () { - a = $(this).children('a'); - input = $(this).children('input'); - a.text(input.val()); - a.removeClass('hidden'); - input.addClass('hidden'); + $('#row-edit-mode-alert').fadeOut(200); }); } else { + users_to_purge = []; $('.edit-control').each(function () { + $(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger'); $(this).removeClass('hidden'); }); - $('.edit-user-control > .edit-user-name').each(function () { - $(this).children('a').addClass('hidden'); - $(this).children('input').removeClass('hidden'); - }); - - } - }); - - $(window).resize(function () { - if ($('.popover').popover().is(':visible')) { - var popover = $('.popover'); - popover.addClass("noTransition"); - $('#purge-message').popover('show'); - popover.removeClass("noTransition"); } }); }); diff --git a/plexpy/__init__.py b/plexpy/__init__.py index ed0c1b5d..44cd2b3c 100644 --- a/plexpy/__init__.py +++ b/plexpy/__init__.py @@ -588,6 +588,12 @@ def dbcheck(): 'ALTER TABLE users ADD COLUMN custom_avatar_url TEXT' ) + # Add "Local" user to database as default unauthenticated user. + result = c_db.execute('SELECT id FROM users WHERE username = "Local"') + if not result.fetchone(): + logger.debug(u'User "Local" does not exist. Adding user.') + c_db.execute('INSERT INTO users (user_id, username) VALUES (0, "Local")') + conn_db.commit() c_db.close() diff --git a/plexpy/config.py b/plexpy/config.py index 5fe59e29..51145685 100644 --- a/plexpy/config.py +++ b/plexpy/config.py @@ -1,4 +1,4 @@ -import plexpy.logger +import plexpy.logger import itertools import os import re @@ -84,6 +84,7 @@ _CONFIG_DEFINITIONS = { 'GROWL_ON_WATCHED': (int, 'Growl', 0), 'HOME_STATS_LENGTH': (int, 'General', 30), 'HOME_STATS_TYPE': (int, 'General', 0), + 'HOME_STATS_COUNT': (int, 'General', 5), 'HTTPS_CERT': (str, 'General', ''), 'HTTPS_KEY': (str, 'General', ''), 'HTTP_HOST': (str, 'General', '0.0.0.0'), diff --git a/plexpy/datafactory.py b/plexpy/datafactory.py index b60c654b..fb587228 100644 --- a/plexpy/datafactory.py +++ b/plexpy/datafactory.py @@ -55,6 +55,7 @@ class DataFactory(object): (CASE WHEN session_history_metadata.duration IS NULL THEN 1.0 ELSE \ session_history_metadata.duration * 1.0 END) * 100) as percent_complete', 'session_history.grandparent_rating_key as grandparent_rating_key', + 'session_history.parent_rating_key as parent_rating_key', 'session_history.rating_key as rating_key', 'session_history.user', 'session_history_metadata.media_type', @@ -112,6 +113,7 @@ class DataFactory(object): "duration": item["duration"], "percent_complete": item["percent_complete"], "grandparent_rating_key": item["grandparent_rating_key"], + "parent_rating_key": item["parent_rating_key"], "rating_key": item["rating_key"], "user": item["user"], "media_type": item["media_type"], @@ -129,7 +131,7 @@ class DataFactory(object): return dict - def get_home_stats(self, time_range='30', stat_type='0'): + def get_home_stats(self, time_range='30', stat_type='0', stat_count='5'): monitor_db = database.MonitorDatabase() if not time_range.isdigit(): @@ -137,8 +139,11 @@ class DataFactory(object): sort_type = 'total_plays' if stat_type == '0' else 'total_duration' + if not time_range.isdigit(): + stat_count = '5' + # This actually determines the output order in the home page - stats_queries = ["top_tv", "popular_tv", "top_movies", "popular_movies", "top_users", "top_platforms"] + stats_queries = ["top_tv", "popular_tv", "top_movies", "popular_movies", "top_users", "top_platforms", "last_watched"] home_stats = [] for stat in stats_queries: @@ -161,7 +166,7 @@ class DataFactory(object): '>= datetime("now", "-%s days", "localtime") ' \ 'AND session_history_metadata.media_type = "episode" ' \ 'GROUP BY session_history_metadata.grandparent_title ' \ - 'ORDER BY %s DESC LIMIT 10' % (time_range, sort_type) + 'ORDER BY %s DESC LIMIT %s' % (time_range, sort_type, stat_count) result = monitor_db.select(query) except: logger.warn("Unable to execute database query.") @@ -207,7 +212,7 @@ class DataFactory(object): '>= datetime("now", "-%s days", "localtime") ' \ 'AND session_history_metadata.media_type = "movie" ' \ 'GROUP BY session_history_metadata.full_title ' \ - 'ORDER BY %s DESC LIMIT 10' % (time_range, sort_type) + 'ORDER BY %s DESC LIMIT %s' % (time_range, sort_type, stat_count) result = monitor_db.select(query) except: logger.warn("Unable to execute database query.") @@ -251,7 +256,7 @@ class DataFactory(object): 'AND session_history_metadata.media_type = "episode" ' \ 'GROUP BY session_history_metadata.grandparent_title ' \ 'ORDER BY users_watched DESC, total_plays DESC ' \ - 'LIMIT 10' % time_range + 'LIMIT %s' % (time_range, stat_count) result = monitor_db.select(query) except: logger.warn("Unable to execute database query.") @@ -293,7 +298,7 @@ class DataFactory(object): 'AND session_history_metadata.media_type = "movie" ' \ 'GROUP BY session_history_metadata.full_title ' \ 'ORDER BY users_watched DESC, total_plays DESC ' \ - 'LIMIT 10' % time_range + 'LIMIT %s' % (time_range, stat_count) result = monitor_db.select(query) except: logger.warn("Unable to execute database query.") @@ -338,7 +343,7 @@ class DataFactory(object): 'WHERE datetime(session_history.stopped, "unixepoch", "localtime") >= ' \ 'datetime("now", "-%s days", "localtime") '\ 'GROUP BY session_history.user_id ' \ - 'ORDER BY %s DESC LIMIT 10' % (time_range, sort_type) + 'ORDER BY %s DESC LIMIT %s' % (time_range, sort_type, stat_count) result = monitor_db.select(query) except: logger.warn("Unable to execute database query.") @@ -356,7 +361,7 @@ class DataFactory(object): 'total_plays': item[2], 'total_duration': item[3], 'last_play': item[4], - 'thumb': user_thumb, + 'user_thumb': user_thumb, 'grandparent_thumb': '', 'users_watched': '', 'rating_key': '', @@ -386,7 +391,7 @@ class DataFactory(object): 'WHERE datetime(session_history.stopped, "unixepoch", "localtime") ' \ '>= datetime("now", "-%s days", "localtime") ' \ 'GROUP BY session_history.platform ' \ - 'ORDER BY total_plays DESC' % time_range + 'ORDER BY %s DESC LIMIT %s' % (time_range, sort_type, stat_count) result = monitor_db.select(query) except: logger.warn("Unable to execute database query.") @@ -413,6 +418,59 @@ class DataFactory(object): 'stat_type': sort_type, 'rows': top_platform}) + elif 'last_watched' in stat: + last_watched = [] + try: + query = 'SELECT session_history_metadata.id, ' \ + 'session_history.user, ' \ + '(case when users.friendly_name is null then session_history.user else ' \ + 'users.friendly_name end) as friendly_name,' \ + 'users.user_id, ' \ + 'users.custom_avatar_url as user_thumb, ' \ + 'session_history_metadata.full_title, ' \ + 'session_history_metadata.rating_key, ' \ + 'session_history_metadata.thumb, ' \ + 'session_history_metadata.grandparent_thumb, ' \ + 'MAX(session_history.started) as last_watch, ' \ + 'session_history.player as platform ' \ + 'FROM session_history_metadata ' \ + 'JOIN session_history ON session_history_metadata.id = session_history.id ' \ + 'LEFT OUTER JOIN users ON session_history.user_id = users.user_id ' \ + 'WHERE datetime(session_history.stopped, "unixepoch", "localtime") ' \ + '>= datetime("now", "-%s days", "localtime") ' \ + 'AND (session_history_metadata.media_type = "movie" ' \ + 'OR session_history_metadata.media_type = "episode") ' \ + 'GROUP BY session_history_metadata.full_title ' \ + 'ORDER BY last_watch DESC ' \ + 'LIMIT %s' % (time_range, stat_count) + result = monitor_db.select(query) + except: + logger.warn("Unable to execute database query.") + return None + + for item in result: + if not item[8] or item[8] == '': + thumb = item[7] + else: + thumb = item[8] + + row = {'row_id': item[0], + 'user': item[1], + 'friendly_name': item[2], + 'user_id': item[3], + 'user_thumb': item[4], + 'title': item[5], + 'rating_key': item[6], + 'thumb': thumb, + 'grandparent_thumb': item[8], + 'last_watch': item[9], + 'platform_type': item[10], + } + last_watched.append(row) + + home_stats.append({'stat_id': stat, + 'rows': last_watched}) + return home_stats def get_stream_details(self, row_id=None): diff --git a/plexpy/plextv.py b/plexpy/plextv.py index b952004b..cac4009a 100644 --- a/plexpy/plextv.py +++ b/plexpy/plextv.py @@ -27,9 +27,8 @@ def refresh_users(): if len(result) > 0: for item in result: - control_value_dict = {"username": item['username']} - new_value_dict = {"user_id": item['user_id'], - "username": item['username'], + control_value_dict = {"user_id": item['user_id']} + new_value_dict = {"username": item['username'], "thumb": item['thumb'], "email": item['email'], "is_home_user": item['is_home_user'], diff --git a/plexpy/pmsconnect.py b/plexpy/pmsconnect.py index 54d2093f..8bb317b0 100644 --- a/plexpy/pmsconnect.py +++ b/plexpy/pmsconnect.py @@ -89,6 +89,23 @@ class PmsConnect(object): return request + """ + Return list of seasons in requested show. + + Parameters required: rating_key { ratingKey of parent } + Optional parameters: output_format { dict, json } + + Output: array + """ + def get_season_list(self, rating_key='', output_format=''): + uri = '/library/metadata/' + rating_key + '/children' + request = self.request_handler.make_request(uri=uri, + proto=self.protocol, + request_type='GET', + output_format=output_format) + + return request + """ Return list of episodes in requested season. @@ -103,7 +120,7 @@ class PmsConnect(object): proto=self.protocol, request_type='GET', output_format=output_format) - + return request """ @@ -154,6 +171,38 @@ class PmsConnect(object): return request + """ + Return list of libraries on server. + + Optional parameters: output_format { dict, json } + + Output: array + """ + def get_libraries_list(self, output_format=''): + uri = '/library/sections' + request = self.request_handler.make_request(uri=uri, + proto=self.protocol, + request_type='GET', + output_format=output_format) + + return request + + """ + Return list of items in library on server. + + Optional parameters: output_format { dict, json } + + Output: array + """ + def get_library_list(self, section_key='', list_type='all', count='0', sort_type='', output_format=''): + uri = '/library/sections/' + section_key + '/' + list_type +'?X-Plex-Container-Start=0&X-Plex-Container-Size=' + count + sort_type + request = self.request_handler.make_request(uri=uri, + proto=self.protocol, + request_type='GET', + output_format=output_format) + + return request + """ Return sync item details. @@ -552,6 +601,7 @@ class PmsConnect(object): 'user': user_details['username'], 'user_id': user_details['user_id'], 'friendly_name': user_details['friendly_name'], + 'user_thumb': user_details['thumb'], 'player': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'title'), 'platform': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'platform'), 'machine_id': machine_id, @@ -661,6 +711,7 @@ class PmsConnect(object): 'user': user_details['username'], 'user_id': user_details['user_id'], 'friendly_name': user_details['friendly_name'], + 'user_thumb': user_details['thumb'], 'player': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'title'), 'platform': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'platform'), 'machine_id': machine_id, @@ -668,6 +719,7 @@ class PmsConnect(object): 'grandparent_title': helpers.get_xml_attr(session, 'grandparentTitle'), 'parent_title': helpers.get_xml_attr(session, 'parentTitle'), 'title': helpers.get_xml_attr(session, 'title'), + 'year': helpers.get_xml_attr(session, 'year'), 'rating_key': helpers.get_xml_attr(session, 'ratingKey'), 'parent_rating_key': helpers.get_xml_attr(session, 'parentRatingKey'), 'grandparent_rating_key': helpers.get_xml_attr(session, 'grandparentRatingKey'), @@ -693,9 +745,13 @@ class PmsConnect(object): 'duration': duration, 'progress': progress, 'progress_percent': str(helpers.get_percent(progress, duration)), - 'type': helpers.get_xml_attr(session, 'type'), 'indexes': use_indexes } + if helpers.get_xml_attr(session, 'ratingKey').isdigit(): + session_output['type'] = helpers.get_xml_attr(session, 'type') + else: + session_output['type'] = 'clip' + elif helpers.get_xml_attr(session, 'type') == 'movie': session_output = {'session_key': helpers.get_xml_attr(session, 'sessionKey'), 'media_index': helpers.get_xml_attr(session, 'index'), @@ -708,6 +764,7 @@ class PmsConnect(object): 'user': user_details['username'], 'user_id': user_details['user_id'], 'friendly_name': user_details['friendly_name'], + 'user_thumb': user_details['thumb'], 'player': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'title'), 'platform': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'platform'), 'machine_id': machine_id, @@ -715,6 +772,7 @@ class PmsConnect(object): 'grandparent_title': helpers.get_xml_attr(session, 'grandparentTitle'), 'parent_title': helpers.get_xml_attr(session, 'parentTitle'), 'title': helpers.get_xml_attr(session, 'title'), + 'year': helpers.get_xml_attr(session, 'year'), 'rating_key': helpers.get_xml_attr(session, 'ratingKey'), 'parent_rating_key': helpers.get_xml_attr(session, 'parentRatingKey'), 'grandparent_rating_key': helpers.get_xml_attr(session, 'grandparentRatingKey'), @@ -740,9 +798,13 @@ class PmsConnect(object): 'duration': duration, 'progress': progress, 'progress_percent': str(helpers.get_percent(progress, duration)), - 'type': helpers.get_xml_attr(session, 'type'), 'indexes': use_indexes } + if helpers.get_xml_attr(session, 'ratingKey').isdigit(): + session_output['type'] = helpers.get_xml_attr(session, 'type') + else: + session_output['type'] = 'clip' + elif helpers.get_xml_attr(session, 'type') == 'clip': session_output = {'session_key': helpers.get_xml_attr(session, 'sessionKey'), 'media_index': helpers.get_xml_attr(session, 'index'), @@ -755,6 +817,7 @@ class PmsConnect(object): 'user': user_details['username'], 'user_id': user_details['user_id'], 'friendly_name': user_details['friendly_name'], + 'user_thumb': user_details['thumb'], 'player': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'title'), 'platform': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'platform'), 'machine_id': machine_id, @@ -762,6 +825,7 @@ class PmsConnect(object): 'grandparent_title': helpers.get_xml_attr(session, 'grandparentTitle'), 'parent_title': helpers.get_xml_attr(session, 'parentTitle'), 'title': helpers.get_xml_attr(session, 'title'), + 'year': helpers.get_xml_attr(session, 'year'), 'rating_key': helpers.get_xml_attr(session, 'ratingKey'), 'parent_rating_key': helpers.get_xml_attr(session, 'parentRatingKey'), 'grandparent_rating_key': helpers.get_xml_attr(session, 'grandparentRatingKey'), @@ -795,6 +859,49 @@ class PmsConnect(object): return session_output + """ + Return processed and validated season list. + + Output: array + """ + def get_show_children(self, rating_key=''): + season_data = self.get_season_list(rating_key, output_format='xml') + + try: + xml_head = season_data.getElementsByTagName('MediaContainer') + except: + logger.warn("Unable to parse XML for get_season_list.") + return [] + + season_list = [] + + for a in xml_head: + if a.getAttribute('size'): + if a.getAttribute('size') == '0': + logger.debug(u"No season data.") + season_list = {'season_count': '0', + 'season_list': [] + } + return season_list + + if a.getElementsByTagName('Directory'): + result_data = a.getElementsByTagName('Directory') + for result in result_data: + season_output = {'rating_key': helpers.get_xml_attr(result, 'ratingKey'), + 'index': helpers.get_xml_attr(result, 'index'), + 'title': helpers.get_xml_attr(result, 'title'), + 'thumb': helpers.get_xml_attr(result, 'thumb'), + 'parent_thumb': helpers.get_xml_attr(a, 'thumb') + } + season_list.append(season_output) + + output = {'season_count': helpers.get_xml_attr(xml_head[0], 'size'), + 'title': helpers.get_xml_attr(xml_head[0], 'title2'), + 'season_list': season_list + } + + return output + """ Return processed and validated episode list. @@ -914,6 +1021,150 @@ class PmsConnect(object): logger.debug(u"Server preferences queried but no parameter received.") return None + """ + Return processed and validated server libraries list. + + Output: array + """ + def get_server_children(self): + libraries_data = self.get_libraries_list(output_format='xml') + + try: + xml_head = libraries_data.getElementsByTagName('MediaContainer') + except: + logger.warn("Unable to parse XML for get_libraries_list.") + return [] + + libraries_list = [] + + for a in xml_head: + if a.getAttribute('size'): + if a.getAttribute('size') == '0': + logger.debug(u"No libraries data.") + libraries_list = {'libraries_count': '0', + 'libraries_list': [] + } + return libraries_list + + if a.getElementsByTagName('Directory'): + result_data = a.getElementsByTagName('Directory') + for result in result_data: + libraries_output = {'key': helpers.get_xml_attr(result, 'key'), + 'type': helpers.get_xml_attr(result, 'type'), + 'title': helpers.get_xml_attr(result, 'title'), + 'thumb': helpers.get_xml_attr(result, 'thumb') + } + libraries_list.append(libraries_output) + + output = {'libraries_count': helpers.get_xml_attr(xml_head[0], 'size'), + 'title': helpers.get_xml_attr(xml_head[0], 'title1'), + 'libraries_list': libraries_list + } + + return output + + """ + Return processed and validated server library items list. + + Parameters required: library_type { movie, show, episode, artist } + section_key { unique library key } + + Output: array + """ + def get_library_children(self, library_type='', section_key='', list_type='all', sort_type = ''): + + # Currently only grab the library with 1 items so 'size' is not 0 + count = '1' + + if library_type == 'movie': + sort_type = '&type=1' + elif library_type == 'show': + sort_type = '&type=2' + elif library_type == 'episode': + sort_type = '&type=4' + elif library_type == 'album': + list_type = 'albums' + + library_data = self.get_library_list(section_key, list_type, count, sort_type, output_format='xml') + + try: + xml_head = library_data.getElementsByTagName('MediaContainer') + except: + logger.warn("Unable to parse XML for get_library_children.") + return [] + + library_list = [] + + for a in xml_head: + if a.getAttribute('size'): + if a.getAttribute('size') == '0': + logger.debug(u"No library data.") + library_list = {'library_count': '0', + 'library_list': [] + } + return library_list + + if a.getElementsByTagName('Directory'): + result_data = a.getElementsByTagName('Directory') + for result in result_data: + library_output = {'key': helpers.get_xml_attr(result, 'key'), + 'type': helpers.get_xml_attr(result, 'type'), + 'title': helpers.get_xml_attr(result, 'title'), + 'thumb': helpers.get_xml_attr(result, 'thumb') + } + library_list.append(library_output) + + output = {'library_count': helpers.get_xml_attr(xml_head[0], 'totalSize'), + 'count_type': helpers.get_xml_attr(xml_head[0], 'title2'), + 'library_list': library_list + } + + return output + + """ + Return processed and validated server statistics. + + Output: array + """ + def get_library_stats(self): + server_libraries = self.get_server_children() + + server_library_stats = [] + + if server_libraries['libraries_count'] != '0': + libraries_list = server_libraries['libraries_list'] + + for library in libraries_list: + library_type = library['type'] + section_key = library['key'] + library_list = self.get_library_children(library_type, section_key) + + if library_list['library_count'] != '0': + library_stats = {'title': library['title'], + 'thumb': library['thumb'], + 'count': library_list['library_count'], + 'count_type': library_list['count_type'] + } + + if library_type == 'show': + episode_list = self.get_library_children(library_type='episode', section_key=section_key) + episode_stats = {'episode_count': episode_list['library_count'], + 'episode_count_type': 'All Episodes' + } + library_stats.update(episode_stats) + + if library_type == 'artist': + album_list = self.get_library_children(library_type='album', section_key=section_key) + album_stats = {'album_count': album_list['library_count'], + 'album_count_type': 'All Albums' + } + library_stats.update(album_stats) + + server_library_stats.append({'type': library_type, + 'rows': library_stats}) + + return server_library_stats + """ Return image data as array. Array contains the image content type and image binary diff --git a/plexpy/version.py b/plexpy/version.py index 60c9383f..9fbd0cc4 100644 --- a/plexpy/version.py +++ b/plexpy/version.py @@ -1,2 +1,2 @@ PLEXPY_VERSION = "master" -PLEXPY_RELEASE_VERSION = "1.1.5" +PLEXPY_RELEASE_VERSION = "1.1.6" diff --git a/plexpy/webserve.py b/plexpy/webserve.py index 93092587..5d2ba311 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -1,4 +1,4 @@ -# This file is part of PlexPy. +# This file is part of PlexPy. # # PlexPy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -66,7 +66,9 @@ class WebInterface(object): def home(self): config = { "home_stats_length": plexpy.CONFIG.HOME_STATS_LENGTH, - "home_stats_type": plexpy.CONFIG.HOME_STATS_TYPE + "home_stats_type": plexpy.CONFIG.HOME_STATS_TYPE, + "home_stats_count": plexpy.CONFIG.HOME_STATS_COUNT, + "pms_identifier": plexpy.CONFIG.PMS_IDENTIFIER, } return serve_template(templatename="index.html", title="Home", config=config) @@ -119,12 +121,19 @@ class WebInterface(object): return json.dumps(formats) @cherrypy.expose - def home_stats(self, time_range='30', stat_type='0', **kwargs): + def home_stats(self, time_range='30', stat_type='0', stat_count='5', **kwargs): data_factory = datafactory.DataFactory() - stats_data = data_factory.get_home_stats(time_range=time_range, stat_type=stat_type) + stats_data = data_factory.get_home_stats(time_range=time_range, stat_type=stat_type, stat_count=stat_count) return serve_template(templatename="home_stats.html", title="Stats", data=stats_data) + @cherrypy.expose + def library_stats(self, **kwargs): + pms_connect = pmsconnect.PmsConnect() + stats_data = pms_connect.get_library_stats() + + return serve_template(templatename="library_stats.html", title="Library Stats", data=stats_data) + @cherrypy.expose def history(self): return serve_template(templatename="history.html", title="History") @@ -453,6 +462,7 @@ class WebInterface(object): "notify_on_watched_body_text": plexpy.CONFIG.NOTIFY_ON_WATCHED_BODY_TEXT, "home_stats_length": plexpy.CONFIG.HOME_STATS_LENGTH, "home_stats_type": checked(plexpy.CONFIG.HOME_STATS_TYPE), + "home_stats_count": plexpy.CONFIG.HOME_STATS_COUNT, "buffer_threshold": plexpy.CONFIG.BUFFER_THRESHOLD, "buffer_wait": plexpy.CONFIG.BUFFER_WAIT } @@ -567,6 +577,9 @@ class WebInterface(object): if 'rating_key' in kwargs: rating_key = kwargs.get('rating_key', "") custom_where = [['rating_key', rating_key]] + if 'parent_rating_key' in kwargs: + rating_key = kwargs.get('parent_rating_key', "") + custom_where = [['parent_rating_key', rating_key]] if 'grandparent_rating_key' in kwargs: rating_key = kwargs.get('grandparent_rating_key', "") custom_where = [['grandparent_rating_key', rating_key]] @@ -747,16 +760,19 @@ class WebInterface(object): @cherrypy.expose def info(self, item_id=None, source=None, **kwargs): + metadata = None if source == 'history': data_factory = datafactory.DataFactory() - result = data_factory.get_metadata_details(row_id=item_id) + metadata = data_factory.get_metadata_details(row_id=item_id) else: pms_connect = pmsconnect.PmsConnect() - result = pms_connect.get_metadata_details(rating_key=item_id)['metadata'] + result = pms_connect.get_metadata_details(rating_key=item_id) + if result: + metadata = result['metadata'] - if result: - return serve_template(templatename="info.html", data=result, title="Info") + if metadata: + return serve_template(templatename="info.html", data=metadata, title="Info") else: logger.warn('Unable to retrieve data.') return serve_template(templatename="info.html", data=None, title="Info") @@ -801,7 +817,19 @@ class WebInterface(object): return serve_template(templatename="user_platform_stats.html", data=None, title="Platform Stats") @cherrypy.expose - def get_children(self, rating_key='', **kwargs): + def get_show_children(self, rating_key='', **kwargs): + + pms_connect = pmsconnect.PmsConnect() + result = pms_connect.get_show_children(rating_key) + + if result: + return serve_template(templatename="info_season_list.html", data=result, title="Season List") + else: + logger.warn('Unable to retrieve data.') + return serve_template(templatename="info_season_list.html", data=None, title="Season List") + + @cherrypy.expose + def get_season_children(self, rating_key='', **kwargs): pms_connect = pmsconnect.PmsConnect() result = pms_connect.get_season_children(rating_key)