This commit is contained in:
herby2212 2025-05-10 16:35:21 -07:00 committed by GitHub
commit c4a1c92128
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 241 additions and 63 deletions

View file

@ -2941,6 +2941,12 @@ a .home-platforms-list-cover-face:hover
max-width: 1750px;
display: flow-root;
}
.table-card-header.spaced {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
align-content: center;
}
.table-card-back td {
font-size: 12px;
}
@ -2952,6 +2958,18 @@ a .home-platforms-list-cover-face:hover
font-weight: bold;
line-height: 34px;
}
.info-bar {
display: inline;
}
.info-element {
display: inline-block;
border-radius: 1rem;
border: 0.2rem solid #242424;
padding: 0.7rem;
background-color: #3B3B3B;
font-style: italic;
color: #676767;
}
.button-bar {
float: right;
}

View file

@ -202,6 +202,30 @@ libraries_list_table_options = {
"searchable": false,
"width": "10%",
"className": "no-wrap"
},
{
"targets": [11],
"data": "total_storage",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== null && cellData !== '') {
$(td).html(humanFileSize(cellData));
}
},
"searchable": false,
"width": "10%",
"className": "no-wrap"
},
{
"targets": [12],
"data": "total_duration",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== null && cellData !== '') {
$(td).html(humanDuration(cellData));
}
},
"searchable": false,
"width": "10%",
"className": "no-wrap"
}
],

View file

@ -224,6 +224,22 @@ media_info_table_options = {
},
{
"targets": [10],
"data": "duration",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== null && cellData !== '' && cellData !== 0) {
$(td).html(humanDuration(cellData));
} else {
if (rowData['section_type'] != 'photo' && get_file_sizes != null) {
get_file_sizes = true;
}
}
},
"width": "7%",
"className": "no-wrap",
"searchable": false
},
{
"targets": [11],
"data": "last_played",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== null && cellData !== '') {
@ -236,7 +252,7 @@ media_info_table_options = {
"searchable": false
},
{
"targets": [11],
"targets": [12],
"data": "play_count",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== null && cellData !== '') {
@ -457,6 +473,7 @@ function childTableFormatMedia(rowData) {
'<th align="left" id="audio_codec">Audio Codec</th>' +
'<th align="left" id="audio_channels">Audio Channels</th>' +
'<th align="left" id="file_size">File Size</th>' +
'<th align="left" id="duration">Duration</th>' +
'<th align="left" id="last_played">Last Played</th>' +
'<th align="left" id="total_plays">Total Plays</th>' +
'</tr>' +

View file

@ -41,6 +41,8 @@
<th align="left" id="last_played">Last Played</th>
<th align="left" id="total_plays">Total Plays</th>
<th align="left" id="total_duration">Total Played Duration</th>
<th align="left" id="total_storage">Total Storage</th>
<th align="left" id="total_runtime">Total Runtime</th>
</tr>
</thead>
<tbody>

View file

@ -271,7 +271,7 @@ DOCUMENTATION :: END
You may leave this page and check back later.
</div>
% endif
<div class='table-card-header'>
<div class='table-card-header spaced'>
<div class="header-bar">
<span>
<i class="fa fa-info-circle"></i> Media Info for <strong>
@ -279,6 +279,14 @@ DOCUMENTATION :: END
</strong>
</span>
</div>
<div class="info-bar">
<div class="info-element">
<span>Total File Size: <strong><span id="info-element-total-storage" /></strong></span>
</div>
<div class="info-element">
<span>Total Media Runtime: <strong><span id="info-element-total-runtime" /></strong></span>
</div>
</div>
<div class="button-bar">
% if _session['user_group'] == 'admin':
<div class="btn-group">
@ -310,6 +318,7 @@ DOCUMENTATION :: END
<th align="left" id="audio_codec">Audio Codec</th>
<th align="left" id="audio_channels">Audio Channels</th>
<th align="left" id="file_size">File Size</th>
<th align="left" id="duration">Duration</th>
<th align="left" id="last_played">Last Played</th>
<th align="left" id="total_plays">Total Plays</th>
</tr>
@ -779,6 +788,22 @@ DOCUMENTATION :: END
clearSearchButton('media_info_table-SID-${data["section_id"]}', media_info_table);
}
$(document).ready(function () {
loadLibraryMediaStats();
});
function loadLibraryMediaStats(refresh) {
// Populate media stats
$.ajax({
url: 'get_library_media_stats',
data: { section_id: section_id, refresh: refresh },
complete: function(xhr, status) {
$("#info-element-total-runtime").html(humanDuration(xhr.responseJSON.total_duration));
$("#info-element-total-storage").html(humanFileSize(xhr.responseJSON.total_storage));
}
});
}
$('#nav-tabs-mediainfo').on('shown.bs.tab', function() {
if (typeof(media_info_table) === 'undefined') {
loadMediaInfoTable();
@ -790,6 +815,7 @@ DOCUMENTATION :: END
refresh_table = true;
refresh_child_tables = true;
media_info_table.draw();
loadLibraryMediaStats(true);
refresh_table = false;
});

View file

@ -75,6 +75,8 @@ def refresh_libraries():
if result == 'insert':
new_keys.append(section['section_id'])
get_library_media_stats(section_id=section['section_id'], refresh=True)
add_live_tv_library(refresh=True)
query = "UPDATE library_sections SET is_active = 0 WHERE server_id != ? OR " \
@ -125,6 +127,44 @@ def has_library_type(section_type):
result = monitor_db.select_single(query=query, args=args)
return bool(result)
def get_library_media_stats(section_id=None, refresh=False):
plex = Plex(token=session.get_session_user_token())
libraries = Libraries()
default_return = {
'total_size': 0,
'total_storage': 0,
'total_duration': 0
}
if section_id and not str(section_id).isdigit():
logger.warn("Tautulli Libraries :: Library media stats requested but invalid section_id provided.")
return default_return
if not session.allow_session_library(section_id):
logger.warn("Tautulli Libraries :: Library media stats requested but library is not allowed for this session.")
return default_return
# Import media info cache from json file
_, cached_library_media_stats, _ = libraries._load_data_from_cache(section_id=section_id, path='media_stats')
_live_data = not cached_library_media_stats or refresh
if _live_data:
library = plex.get_library(section_id)
if library is None:
logger.warn("Tautulli Libraries :: Library media stats requested but no library was found section_id %s.", section_id)
return default_return
library_media_stats = {
'total_size': library.totalSize if _live_data else cached_library_media_stats.get('total_size', 0),
'total_storage': library.totalStorage if _live_data else cached_library_media_stats.get('total_storage', 0),
'total_duration': library.totalDuration if _live_data else cached_library_media_stats.get('total_duration', 0)
}
libraries._save_data_to_cache(section_id=section_id, rows=library_media_stats, path='media_stats')
return library_media_stats
def get_collections(section_id=None):
plex = Plex(token=session.get_session_user_token())
@ -391,18 +431,23 @@ class Libraries(object):
else:
library_art = item['library_art']
library_media_stats = get_library_media_stats(item['section_id'])
row = {'row_id': item['row_id'],
'server_id': item['server_id'],
'section_id': item['section_id'],
'section_name': item['section_name'],
'section_type': item['section_type'],
'count': item['count'],
'total_size': library_media_stats['total_size'],
'parent_count': item['parent_count'],
'child_count': item['child_count'],
'library_thumb': library_thumb,
'library_art': library_art,
'plays': item['plays'],
'total_storage': library_media_stats['total_storage'],
'duration': item['duration'],
'total_duration': library_media_stats['total_duration'],
'last_accessed': item['last_accessed'],
'history_row_id': item['history_row_id'],
'last_played': item['last_played'],
@ -441,6 +486,7 @@ class Libraries(object):
'data': [],
'filtered_file_size': 0,
'total_file_size': 0,
'total_media_duration': 0,
'last_refreshed': None}
if not session.allow_session_library(section_id):
@ -497,10 +543,13 @@ class Libraries(object):
'play_count': item['play_count']}
# Import media info cache from json file
cache_time, rows, library_count = self._load_media_info_cache(section_id=section_id, rating_key=rating_key)
cache_time, rows, library_count = self._load_data_from_cache(section_id=section_id, rating_key=rating_key, path='media_info')
# Check if duration is also included in cache else refresh cache to prevent update issues
refresh = refresh if None not in {d.get('duration') for d in rows} else True
# If no cache was imported, get all library children items
cached_items = {d['rating_key']: d['file_size'] for d in rows} if not refresh else {}
cached_items = {d['rating_key']: [d['file_size'], d['duration']] for d in rows} if not refresh else {}
if refresh or not rows:
pms_connect = pmsconnect.PmsConnect()
@ -525,8 +574,10 @@ class Libraries(object):
for item in children_list:
## TODO: Check list of media info items, currently only grabs first item
cached_file_size = cached_items.get(item['rating_key'], None)
file_size = cached_file_size if cached_file_size else item.get('file_size', '')
cached_item_data = cached_items.get(item['rating_key'], None)
file_size = cached_item_data['file_size'] if cached_item_data else item.get('file_size', '')
duration = cached_item_data['duration'] if cached_item_data else item['duration']
row = {'section_id': library_details['section_id'],
'section_type': library_details['section_type'],
@ -548,7 +599,8 @@ class Libraries(object):
'video_framerate': item.get('video_framerate', ''),
'audio_codec': item.get('audio_codec', ''),
'audio_channels': item.get('audio_channels', ''),
'file_size': file_size
'file_size': file_size,
'duration': duration
}
new_rows.append(row)
@ -557,7 +609,7 @@ class Libraries(object):
return default_return
# Cache the media info to a json file
self._save_media_info_cache(section_id=section_id, rating_key=rating_key, rows=rows)
self._save_data_to_cache(section_id=section_id, rating_key=rating_key, rows=rows, path='media_info')
# Update the last_played and play_count
for item in rows:
@ -606,6 +658,7 @@ class Libraries(object):
results = sorted(results, key=lambda k: k[sort_key].lower(), reverse=reverse)
total_file_size = sum([helpers.cast_to_int(d['file_size']) for d in results])
total_media_duration = sum([helpers.cast_to_int(d['duration']) for d in results])
# Paginate results
results = results[json_data['start']:(json_data['start'] + json_data['length'])]
@ -619,6 +672,7 @@ class Libraries(object):
'draw': int(json_data['draw']),
'filtered_file_size': filtered_file_size,
'total_file_size': total_file_size,
'total_media_duration': total_media_duration,
'last_refreshed': cache_time
}
@ -644,19 +698,20 @@ class Libraries(object):
return False
# Import media info cache from json file
_, rows, _ = self._load_media_info_cache(section_id=section_id, rating_key=rating_key)
_, rows, _ = self._load_data_from_cache(section_id=section_id, rating_key=rating_key, path='media_info')
# Get the total file size for each item
if rating_key:
logger.debug("Tautulli Libraries :: Getting file sizes for rating_key %s." % rating_key)
elif section_id:
logger.debug("Tautulli Libraries :: Fetting file sizes for section_id %s." % section_id)
logger.debug("Tautulli Libraries :: Fetching file sizes for section_id %s." % section_id)
pms_connect = pmsconnect.PmsConnect()
for item in rows:
if item['rating_key'] and not item['file_size']:
if item['rating_key'] and (not item['file_size'] or not item['duration']):
file_size = 0
duration = 0
metadata = pms_connect.get_metadata_children_details(rating_key=item['rating_key'],
get_children=True,
@ -673,11 +728,13 @@ class Libraries(object):
media_info['parts'][0])
file_size += helpers.cast_to_int(media_part_info.get('file_size', 0))
duration += helpers.cast_to_int(child_metadata.get('duration', 0))
item['file_size'] = file_size
item['duration'] = duration
# Cache the media info to a json file
self._save_media_info_cache(section_id=section_id, rating_key=rating_key, rows=rows)
self._save_data_to_cache(section_id=section_id, rating_key=rating_key, rows=rows, path='media_info')
if rating_key:
logger.debug("Tautulli Libraries :: File sizes updated for rating_key %s." % rating_key)
@ -686,67 +743,51 @@ class Libraries(object):
return True
def _load_media_info_cache(self, section_id=None, rating_key=None):
def _load_data_from_cache(self, section_id=None, rating_key=None, path=None):
cache_time = None
rows = []
library_count = 0
# Import media info cache from json file
if rating_key:
try:
inFilePath = os.path.join(plexpy.CONFIG.CACHE_DIR,'media_info_%s-%s.json' % (section_id, rating_key))
with open(inFilePath, 'r') as inFile:
data = json.load(inFile)
if isinstance(data, dict):
cache_time = data['last_refreshed']
rows = data['rows']
else:
rows = data
library_count = len(rows)
logger.debug("Tautulli Libraries :: Loaded media info from cache for rating_key %s (%s items)." % (rating_key, library_count))
except IOError as e:
logger.debug("Tautulli Libraries :: No media info cache for rating_key %s." % rating_key)
section_id = str(section_id) if section_id else section_id
rating_key = str(rating_key) if rating_key else rating_key
elif section_id:
try:
inFilePath = os.path.join(plexpy.CONFIG.CACHE_DIR,'media_info_%s.json' % section_id)
with open(inFilePath, 'r') as inFile:
data = json.load(inFile)
if isinstance(data, dict):
cache_time = data['last_refreshed']
rows = data['rows']
else:
rows = data
library_count = len(rows)
logger.debug("Tautulli Libraries :: Loaded media info from cache for section_id %s (%s items)." % (section_id, library_count))
except IOError as e:
logger.debug("Tautulli Libraries :: No media info cache for section_id %s." % section_id)
# Import data cache from json file
try:
inFilePath = os.path.join(plexpy.CONFIG.CACHE_DIR, (path + '_%s%s' % (section_id, ('-' + rating_key) if rating_key else '')))
with open(inFilePath, 'r') as inFile:
data = json.load(inFile)
if isinstance(data, dict):
cache_time = data['last_refreshed']
rows = data['rows']
else:
rows = data
library_count = len(rows)
logger.debug("Tautulli Libraries :: Loaded %s from cache for section_id %s%s (%s items)." %
(path, section_id, (' and rating key ' + rating_key) if rating_key else '', library_count))
except IOError as e:
logger.debug("Tautulli Libraries :: No media info cache for section_id %s%s." %
(section_id, (' and rating key ' + rating_key) if rating_key else ''))
return cache_time, rows, library_count
def _save_media_info_cache(self, section_id=None, rating_key=None, rows=None):
def _save_data_to_cache(self, section_id, rating_key=None, rows=None, path=None):
cache_time = helpers.timestamp()
if rows is None:
rows = []
if rating_key:
try:
outFilePath = os.path.join(plexpy.CONFIG.CACHE_DIR,'media_info_%s-%s.json' % (section_id, rating_key))
with open(outFilePath, 'w') as outFile:
json.dump({'last_refreshed': cache_time, 'rows': rows}, outFile)
logger.debug("Tautulli Libraries :: Saved media info cache for rating_key %s." % rating_key)
except IOError as e:
logger.debug("Tautulli Libraries :: Unable to create cache file for rating_key %s." % rating_key)
elif section_id:
try:
outFilePath = os.path.join(plexpy.CONFIG.CACHE_DIR,'media_info_%s.json' % section_id)
with open(outFilePath, 'w') as outFile:
json.dump({'last_refreshed': cache_time, 'rows': rows}, outFile)
logger.debug("Tautulli Libraries :: Saved media info cache for section_id %s." % section_id)
except IOError as e:
logger.debug("Tautulli Libraries :: Unable to create cache file for section_id %s." % section_id)
section_id = str(section_id) if section_id else section_id
rating_key = str(rating_key) if rating_key else rating_key
try:
outFilePath = os.path.join(plexpy.CONFIG.CACHE_DIR, (path + '_%s%s' % (section_id, ('-' + rating_key) if rating_key else '')))
with open(outFilePath, 'w') as outFile:
json.dump({'last_refreshed': cache_time, 'rows': rows}, outFile)
logger.debug("Tautulli Libraries :: Saved %s cache for section_id %s%s." %
(path, section_id, (' and rating key ' + rating_key) if rating_key else ''))
except IOError as e:
logger.debug("Tautulli Libraries :: Unable to create cache file for section_id %s%s with error %s." %
(section_id, (' and rating key ' + rating_key) if rating_key else '', e))
def set_config(self, section_id=None, custom_thumb='', custom_art='',
do_notify=1, keep_history=1, do_notify_created=1):

View file

@ -50,7 +50,14 @@ class Plex(object):
self.PlexServer = PlexObject(url, token)
def get_library(self, section_id):
return self.PlexServer.library.sectionByID(int(section_id))
from plexapi.exceptions import NotFound
try:
library = self.PlexServer.library.sectionByID(int(section_id))
except NotFound:
library = None
return library
def get_library_items(self, section_id):
return self.get_library(section_id).all()

View file

@ -2912,7 +2912,8 @@ class PmsConnect(object):
'thumb': helpers.get_xml_attr(item, 'thumb'),
'parent_thumb': helpers.get_xml_attr(item, 'thumb'),
'grandparent_thumb': helpers.get_xml_attr(item, 'grandparentThumb'),
'added_at': helpers.get_xml_attr(item, 'addedAt')
'added_at': helpers.get_xml_attr(item, 'addedAt'),
'duration': helpers.get_xml_attr(item, 'duration') if section_type in ('movie', 'episode', 'track') else 0
}
if get_media_info:

View file

@ -498,6 +498,9 @@ class WebInterface(object):
"section_type": "Show",
"server_id": "ds48g4r354a8v9byrrtr697g3g79w",
"thumb": "/library/metadata/153036/thumb/1462175062",
"total_duration": 3048551210,
"total_size": 62,
"total_storage": 1866078986762,
"year": 2016
},
{...},
@ -519,7 +522,9 @@ class WebInterface(object):
("last_accessed", True, False),
("last_played", True, True),
("plays", True, False),
("duration", True, False)]
("duration", True, False),
("total_storage", True, False),
("total_duration", True, False)]
kwargs['json_data'] = build_datatables_json(kwargs, dt_columns, "section_name")
grouping = helpers.bool_true(grouping, return_none=True)
@ -653,6 +658,40 @@ class WebInterface(object):
except:
return "Failed to update library."
@cherrypy.expose
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
@addtoapi()
def get_library_media_stats(self, section_id=None, refresh=''):
""" Get the media stats of a library section on Tautulli.
```
Required parameters:
section_id (str): The id of the Plex library section
refresh (str): "true" to force a refresh of the stats
Optional parameters:
None
Returns:
{
"total_duration": 3048551210,
"total_size": 62,
"total_storage": 1866078986762
}
```
"""
if helpers.bool_true(refresh):
refresh = True
else:
refresh = False
logger.info("Getting library media stats for section %s.", section_id)
result = libraries.get_library_media_stats(section_id=section_id, refresh=refresh)
return result
@cherrypy.expose
@requireAuth()
def library_watch_time_stats(self, section_id=None, **kwargs):
@ -756,12 +795,14 @@ class WebInterface(object):
"recordsFiltered": 82,
"filtered_file_size": 2616760056742,
"total_file_size": 2616760056742,
"total_media_duration": 7947375522,
"data":
[{"added_at": "1403553078",
"audio_channels": "",
"audio_codec": "",
"bitrate": "",
"container": "",
"duration": "",
"file_size": 253660175293,
"grandparent_rating_key": "",
"last_played": 1462380698,
@ -804,6 +845,7 @@ class WebInterface(object):
("video_framerate", True, True),
("audio_codec", True, True),
("audio_channels", True, True),
("duration", True, False),
("file_size", True, False),
("last_played", True, False),
("play_count", True, False)]