Compare commits

...

3 Commits

Author SHA1 Message Date
d05a230554 fix audio player 2025-05-29 19:09:59 +00:00
a66379dc63 cleanup audio player 2025-05-29 18:51:30 +00:00
9023c41cab file_access on seperate page 2025-05-29 18:11:02 +00:00
8 changed files with 281 additions and 131 deletions

View File

@ -378,36 +378,7 @@ def dashboard():
params_for_filter = (start_str, filetype + '%') params_for_filter = (start_str, filetype + '%')
# 1. Top files by access count # 1. Top files by access count
query = f''' # removed and moved to file_access() function
SELECT rel_path, COUNT(*) as access_count
FROM file_access_log
WHERE timestamp >= ? {filetype_filter_sql}
GROUP BY rel_path
ORDER BY access_count DESC
LIMIT 1000
'''
with log_db:
cursor = log_db.execute(query, params_for_filter)
rows = cursor.fetchall()
# Convert rows to a list of dictionaries and add category
rows = [
{
'rel_path': rel_path,
'access_count': access_count,
'category': hf.extract_structure_from_string(rel_path)[0]
}
for rel_path, access_count in rows
]
categories = set(r['category'] for r in rows) # distinct categories
top20 = [
{
'category': categorie,
'files': [r for r in rows if r['category'] == categorie][:20]
}
for categorie in categories
]
# 2. Distinct device trend # 2. Distinct device trend
# We'll group by hour if "today", by day if "7days"/"30days", by month if "365days" # We'll group by hour if "today", by day if "7days"/"30days", by month if "365days"
@ -614,7 +585,6 @@ def dashboard():
return render_template( return render_template(
"dashboard.html", "dashboard.html",
timeframe=session['timeframe'], timeframe=session['timeframe'],
top20 = top20,
distinct_device_data=distinct_device_data, distinct_device_data=distinct_device_data,
user_agent_data=user_agent_data, user_agent_data=user_agent_data,
folder_data=folder_data, folder_data=folder_data,
@ -629,6 +599,84 @@ def dashboard():
title_long=title_long title_long=title_long
) )
@require_secret
def file_access():
if 'timeframe' not in session:
session['timeframe'] = 'last24hours'
session['timeframe'] = request.args.get('timeframe', session['timeframe'])
now = datetime.now()
filetype = 'audio/'
# Determine start time based on session['timeframe']
if session['timeframe'] == 'last24hours':
start_dt = now - timedelta(hours=24)
elif session['timeframe'] == '7days':
start_dt = now - timedelta(days=7)
elif session['timeframe'] == '30days':
start_dt = now - timedelta(days=30)
elif session['timeframe'] == '365days':
start_dt = now - timedelta(days=365)
else:
start_dt = now - timedelta(hours=24)
# We'll compare the textual timestamp (ISO 8601).
start_str = start_dt.isoformat()
# Filter for mimes that start with the given type
filetype_filter_sql = "AND mime LIKE ?"
params_for_filter = (start_str, filetype + '%')
# 1. Top files by access count
query = f'''
SELECT rel_path, COUNT(*) as access_count
FROM file_access_log
WHERE timestamp >= ? {filetype_filter_sql}
GROUP BY rel_path
ORDER BY access_count DESC
LIMIT 1000
'''
with log_db:
cursor = log_db.execute(query, params_for_filter)
rows = cursor.fetchall()
# Convert rows to a list of dictionaries and add category
rows = [
{
'rel_path': rel_path,
'access_count': access_count,
'category': hf.extract_structure_from_string(rel_path)[0]
}
for rel_path, access_count in rows
]
# Get possible categories from the rows
categories = sorted({r['category'] for r in rows if r['category'] is not None})
all_categories = [None] + categories
top20 = []
for category in all_categories:
label = category if category is not None else 'Keine Kategorie gefunden !'
files = [r for r in rows if r['category'] == category][:20]
top20.append({
'category': label,
'files': files
})
title_short = app_config.get('TITLE_SHORT', 'Default Title')
title_long = app_config.get('TITLE_LONG' , 'Default Title')
return render_template(
"file_access.html",
timeframe=session['timeframe'],
top20 = top20,
admin_enabled=auth.is_admin(),
title_short=title_short,
title_long=title_long
)
def export_to_excel(): def export_to_excel():
"""Export search_db to an Excel file and store it locally.""" """Export search_db to an Excel file and store it locally."""

3
app.py
View File

@ -43,6 +43,7 @@ if os.environ.get('FLASK_ENV') == 'production':
app.config['SESSION_COOKIE_SECURE'] = True app.config['SESSION_COOKIE_SECURE'] = True
app.add_url_rule('/dashboard', view_func=a.dashboard) app.add_url_rule('/dashboard', view_func=a.dashboard)
app.add_url_rule('/file_access', view_func=a.file_access)
app.add_url_rule('/connections', view_func=a.connections) app.add_url_rule('/connections', view_func=a.connections)
app.add_url_rule('/mylinks', view_func=auth.mylinks) app.add_url_rule('/mylinks', view_func=auth.mylinks)
app.add_url_rule('/remove_secret', view_func=auth.remove_secret, methods=['POST']) app.add_url_rule('/remove_secret', view_func=auth.remove_secret, methods=['POST'])
@ -206,6 +207,8 @@ def generate_breadcrumbs(subpath=None):
path_accum = "" path_accum = ""
for part in parts: for part in parts:
path_accum = f"{path_accum}/{part}" if path_accum else part path_accum = f"{path_accum}/{part}" if path_accum else part
if 'toplist' in part:
part = part.replace('toplist', 'oft angehört')
breadcrumbs.append({'name': part, 'path': path_accum}) breadcrumbs.append({'name': part, 'path': path_accum})
return breadcrumbs return breadcrumbs

View File

@ -124,9 +124,12 @@ function renderContent(data) {
if (admin_enabled && data.breadcrumbs.length != 1 && dir.share) { if (admin_enabled && data.breadcrumbs.length != 1 && dir.share) {
share_link = `<a href="#" class="create-share" data-url="${dir.path}">⚙️</a>`; share_link = `<a href="#" class="create-share" data-url="${dir.path}">⚙️</a>`;
} }
contentHTML += `<li class="directory-item"><a href="#" class="directory-link" data-path="${dir.path}">📁 ${dir.name}</a> if (dir.path.includes('toplist')) {
${share_link} link_symbol = '⭐';
</li>`; } else {
link_symbol = '📁';
}
contentHTML += `<li class="directory-item"><a href="#" class="directory-link" data-path="${dir.path}">${link_symbol} ${dir.name}</a>${share_link}</li>`;
}); });
if (data.breadcrumbs.length === 1) { if (data.breadcrumbs.length === 1) {
contentHTML += `<li class="link-item" onclick="viewSearch()"><a onclick="viewSearch() class="link-link">🔎 Suche</a></li>`; contentHTML += `<li class="link-item" onclick="viewSearch()"><a onclick="viewSearch() class="link-link">🔎 Suche</a></li>`;
@ -453,42 +456,6 @@ function reloadDirectory() {
} }
if ('mediaSession' in navigator) {
// Handler for the play action
navigator.mediaSession.setActionHandler('play', () => {
document.getElementById('globalAudio').play();
});
// Handler for the pause action
navigator.mediaSession.setActionHandler('pause', () => {
document.getElementById('globalAudio').pause();
});
// Handler for the previous track action
navigator.mediaSession.setActionHandler('previoustrack', () => {
if (currentMusicIndex > 0) {
const prevFile = currentMusicFiles[currentMusicIndex - 1];
const prevLink = document.querySelector(`.play-file[data-url="${prevFile.path}"]`);
if (prevLink) {
prevLink.click();
}
}
});
// Handler for the next track action
navigator.mediaSession.setActionHandler('nexttrack', () => {
if (currentMusicIndex >= 0 && currentMusicIndex < currentMusicFiles.length - 1) {
const nextFile = currentMusicFiles[currentMusicIndex + 1];
const nextLink = document.querySelector(`.play-file[data-url="${nextFile.path}"]`);
if (nextLink) {
nextLink.click();
}
}
});
}
document.getElementById('globalAudio').addEventListener('ended', () => { document.getElementById('globalAudio').addEventListener('ended', () => {
reloadDirectory().then(() => { reloadDirectory().then(() => {

View File

@ -3,11 +3,10 @@ const cssVar = getComputedStyle(document.documentElement).getPropertyValue('--da
// player DOM elements // player DOM elements
const nowPlayingInfo = document.getElementById('nowPlayingInfo'); const nowPlayingInfo = document.getElementById('nowPlayingInfo');
const audioPlayer = document.getElementById('globalAudio'); const audio = document.getElementById('globalAudio');
const audioPlayerContainer = document.getElementById('audioPlayerContainer'); const audioPlayerContainer = document.getElementById('audioPlayerContainer');
const playerButton = document.querySelector('.player-button'), const playerButton = document.querySelector('.player-button'),
audio = document.querySelector('audio'),
timeline = document.querySelector('.timeline'), timeline = document.querySelector('.timeline'),
timeInfo = document.getElementById('timeInfo'), timeInfo = document.getElementById('timeInfo'),
playIcon = ` playIcon = `
@ -57,10 +56,24 @@ timeline.addEventListener('touchcancel', () => { isSeeking = false; });
audio.addEventListener('loadedmetadata', () => { audio.addEventListener('loadedmetadata', () => {
timeline.min = 0; timeline.min = 0;
timeline.max = audio.duration; timeline.max = audio.duration;
timeline.value = 0;
timeline.style.backgroundSize = '0% 100%';
}); });
audio.addEventListener('play', () => {
if ('mediaSession' in navigator)
navigator.mediaSession.playbackState = 'playing';
});
audio.addEventListener('pause', () => {
if ('mediaSession' in navigator)
navigator.mediaSession.playbackState = 'paused';
});
// --- Seek function: directly set audio.currentTime using slider's value (in seconds) --- // --- Seek function: directly set audio.currentTime using slider's value (in seconds) ---
function changeSeek() { function changeSeek() {
audio.currentTime = timeline.value; audio.currentTime = timeline.value;
@ -75,30 +88,20 @@ function formatTime(seconds) {
// --- Update timeline, time info, and media session on each time update --- // --- Update timeline, time info, and media session on each time update ---
audio.ontimeupdate = function() { function updateTimeline() {
if (!isSeeking && audio.duration) {
// --- Update the slider thumb position (in seconds) while playing ---
if (!isSeeking) {
timeline.value = audio.currentTime; timeline.value = audio.currentTime;
const percentagePosition = (audio.currentTime / audio.duration) * 100; timeline.style.backgroundSize = `${(audio.currentTime / audio.duration) * 100}% 100%`;
timeline.style.backgroundSize = `${percentagePosition}% 100%`; timeInfo.textContent = `${formatTime(audio.currentTime)} / ${formatTime(audio.duration)}`;
}
} }
// --- Update the time display --- audio.ontimeupdate = updateTimeline;
const currentTimeFormatted = formatTime(audio.currentTime);
const durationFormatted = isNaN(audio.duration) ? "00:00" : formatTime(audio.duration);
timeInfo.innerHTML = `${currentTimeFormatted} / ${durationFormatted}`;
if ('mediaSession' in navigator && // Fallback for mobile throttling
'setPositionState' in navigator.mediaSession && setInterval(() => {
!isNaN(audio.duration)) { if (!audio.paused && !isSeeking) updateTimeline();
navigator.mediaSession.setPositionState({ }, 500);
duration: audio.duration,
playbackRate: audio.playbackRate,
position: audio.currentTime
});
}
};
// --- When audio ends --- // --- When audio ends ---
@ -107,7 +110,6 @@ audio.onended = function() {
}; };
async function downloadAudio() { async function downloadAudio() {
const audio = document.getElementById('globalAudio');
const src = audio.currentSrc || audio.src; const src = audio.currentSrc || audio.src;
if (!src) return; if (!src) return;
@ -132,8 +134,8 @@ let currentFetchController = null;
async function startPlaying(relUrl) { async function startPlaying(relUrl) {
// Pause the audio and clear its source. // Pause the audio and clear its source.
audioPlayer.pause(); audio.pause();
audioPlayer.src = ''; audio.src = '';
// Display the audio player container. // Display the audio player container.
audioPlayerContainer.style.display = "block"; audioPlayerContainer.style.display = "block";
@ -171,9 +173,9 @@ async function startPlaying(relUrl) {
} }
// Set the media URL, load, and play the audio. // Set the media URL, load, and play the audio.
audioPlayer.src = mediaUrl; audio.src = mediaUrl;
audioPlayer.load(); audio.load();
await audioPlayer.play(); await audio.play();
clearTimeout(spinnerTimer); clearTimeout(spinnerTimer);
hideSpinner(); hideSpinner();
currentTrackPath = relUrl; currentTrackPath = relUrl;
@ -194,6 +196,12 @@ async function startPlaying(relUrl) {
{ src: '/icons/logo-192x192.png', sizes: '192x192', type: 'image/png' } { src: '/icons/logo-192x192.png', sizes: '192x192', type: 'image/png' }
] ]
}); });
navigator.mediaSession.playbackState = 'playing';
navigator.mediaSession.setPositionState({
duration: audio.duration,
playbackRate: audio.playbackRate,
position: 0
});
} }
nowPlayingInfo.innerHTML = pathStr.replace(/\//g, ' > ') + nowPlayingInfo.innerHTML = pathStr.replace(/\//g, ' > ') +
@ -208,3 +216,57 @@ async function startPlaying(relUrl) {
} }
}; };
} }
if ('mediaSession' in navigator) {
// Handler for the play action
navigator.mediaSession.setActionHandler('play', () => {
navigator.mediaSession.playbackState = 'playing'
document.getElementById('globalAudio').play();
});
// Handler for the pause action
navigator.mediaSession.setActionHandler('pause', () => {
navigator.mediaSession.playbackState = 'paused'
document.getElementById('globalAudio').pause();
});
// Handler for the previous track action
navigator.mediaSession.setActionHandler('previoustrack', () => {
if (currentMusicIndex > 0) {
const prevFile = currentMusicFiles[currentMusicIndex - 1];
const prevLink = document.querySelector(`.play-file[data-url="${prevFile.path}"]`);
if (prevLink) {
prevLink.click();
}
}
});
// Handler for the next track action
navigator.mediaSession.setActionHandler('nexttrack', () => {
if (currentMusicIndex >= 0 && currentMusicIndex < currentMusicFiles.length - 1) {
const nextFile = currentMusicFiles[currentMusicIndex + 1];
const nextLink = document.querySelector(`.play-file[data-url="${nextFile.path}"]`);
if (nextLink) {
nextLink.click();
}
}
});
// Handler for the seek backward action
navigator.mediaSession.setActionHandler('seekto', ({seekTime, fastSeek}) => {
if (fastSeek && 'fastSeek' in audio) {
audio.fastSeek(seekTime);
} else {
audio.currentTime = seekTime;
}
// immediately update the remote clock
navigator.mediaSession.setPositionState({
duration: audio.duration,
playbackRate: audio.playbackRate,
position: audio.currentTime
});
});
}

View File

@ -45,6 +45,8 @@
<span> | </span> <span> | </span>
<a href="{{ url_for('dashboard') }}">Dashbord</a> <a href="{{ url_for('dashboard') }}">Dashbord</a>
<span> | </span> <span> | </span>
<a href="{{ url_for('file_access') }}">Dateizugriffe</a>
<span> | </span>
<a href="{{ url_for('songs_dashboard') }}">Wiederholungen</a> <a href="{{ url_for('songs_dashboard') }}">Wiederholungen</a>
<span> | </span> <span> | </span>
<a href="{{ url_for('folder_secret_config_editor') }}" id="edit-folder-config">Ordnerkonfiguration</a> <a href="{{ url_for('folder_secret_config_editor') }}" id="edit-folder-config">Ordnerkonfiguration</a>

View File

@ -210,38 +210,7 @@
</div> </div>
</div> </div>
<!-- Detailed Table of Top File Accesses -->
{% for top20_item in top20 %}
<div class="card mb-4">
<div class="card-header">
Top 20 Dateizugriffe ({{ top20_item['category'] }})
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Access Count</th>
<th>File Path</th>
</tr>
</thead>
<tbody>
{% for row in top20_item['files'] %}
<tr>
<td>{{ row.access_count }}</td>
<td>{{ row.rel_path }}</td>
</tr>
{% else %}
<tr>
<td colspan="2">No data available for the selected timeframe.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endfor %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,99 @@
{# templates/file_access.html #}
{% extends 'base.html' %}
{# page title #}
{% block title %}Dateizugriffe{% endblock %}
{# page content #}
{% block content %}
<!-- Main Container -->
<div class="container">
<h2>Auswertung-Dateizugriffe</h2>
<!-- Dropdown Controls -->
<div class="mb-4 d-flex flex-wrap gap-2">
<!-- Timeframe Dropdown -->
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle"
type="button" id="timeframeDropdown" data-bs-toggle="dropdown" aria-expanded="false">
{% if session['timeframe'] == 'last24hours' %}
Last 24 Hours
{% elif session['timeframe'] == '7days' %}
Last 7 Days
{% elif session['timeframe'] == '30days' %}
Last 30 Days
{% elif session['timeframe'] == '365days' %}
Last 365 Days
{% else %}
Select Timeframe
{% endif %}
</button>
<ul class="dropdown-menu" aria-labelledby="timeframeDropdown">
<li>
<a class="dropdown-item {% if session['timeframe'] == 'last24hours' %}active{% endif %}"
href="{{ url_for('file_access', timeframe='last24hours') }}">
Last 24 Hours
</a>
</li>
<li>
<a class="dropdown-item {% if session['timeframe'] == '7days' %}active{% endif %}"
href="{{ url_for('file_access', timeframe='7days') }}">
Last 7 Days
</a>
</li>
<li>
<a class="dropdown-item {% if session['timeframe'] == '30days' %}active{% endif %}"
href="{{ url_for('file_access', timeframe='30days') }}">
Last 30 Days
</a>
</li>
<li>
<a class="dropdown-item {% if session['timeframe'] == '365days' %}active{% endif %}"
href="{{ url_for('file_access', timeframe='365days') }}">
Last 365 Days
</a>
</li>
</ul>
</div>
</div>
<!-- Detailed Table of Top File Accesses -->
{% for top20_item in top20 %}
<div class="card mb-4">
<div class="card-header">
Top 20 Dateizugriffe ({{ top20_item['category'] }})
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Access Count</th>
<th>File Path</th>
</tr>
</thead>
<tbody>
{% for row in top20_item['files'] %}
<tr>
<td>{{ row.access_count }}</td>
<td>{{ row.rel_path }}</td>
</tr>
{% else %}
<tr>
<td colspan="2">No data available for the selected timeframe.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endfor %}
</div>
{% endblock %}
{% block scripts %}
{% endblock %}

View File

@ -8,7 +8,7 @@
{% block content %} {% block content %}
<!-- Main Container --> <!-- Main Container -->
<div class="container-fluid px-4"> <div class="container">
<h2>Analyse Wiederholungen</h2> <h2>Analyse Wiederholungen</h2>
<!-- Dropdown Controls --> <!-- Dropdown Controls -->
<div class="mb-4 d-flex flex-wrap gap-2"> <div class="mb-4 d-flex flex-wrap gap-2">