feat: Audiobookshelf integration — browse, stream, chapters, sleep timer

- src/abs.js: full ABS API client + player engine
  - Login/token auth, library browsing, search
  - Playback session API (multi-file book support via startOffset)
  - Progress sync to ABS every 30s and on pause
  - Chapter navigation (prev/next/seek-to), ±10s / ±30s skip
  - Speed control (0.75–2×), sleep timer with countdown
- index.html: sidebar nav, audiobooks/detail pages, full-screen player overlay,
  ABS settings section (URL + credentials + library picker)
- state.js: abs + absPlayer state objects
- views.js: navigate() handles 'audiobooks' and 'abs-book-detail'
- styles.css: book grid, detail page, player overlay, pickers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-05-03 14:35:00 +02:00
1 parent 719d4d9afd
commit c17b7ea11a
7 files changed
+948

No files matched your search

+165
View File
@@ -91,6 +91,12 @@
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M8 11.973c2.5-1.473 5.5-.973 7.5.527M9.5 14.473c1.5-.973 3.5-.973 5 .027M7 9c3-1.5 6.5-1 9 1"/></svg>Spotify Importer
</div>
</div>
<div class="nav-section" id="nav-abs-section" style="display:none">
<div class="nav-section-title">Hörbücher</div>
<div class="nav-item" onclick="navigate('audiobooks')" data-page="audiobooks">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>Bibliothek
</div>
</div>
</nav>
</aside>
@@ -301,6 +307,28 @@
</div>
</div>
<!-- AUDIOBOOKS -->
<div class="page" id="page-audiobooks">
<div class="page-header">
<div class="page-title">Hörbücher <em>📚</em></div>
</div>
<div class="search-wrap" style="padding:0 0 16px">
<div class="search-box" style="background:var(--surface);border-color:var(--border2)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
<input id="abs-search-input" type="text" placeholder="Titel oder Autor suchen…" oninput="absOnSearch(this.value)">
</div>
</div>
<div id="abs-books-grid" class="abs-books-grid"></div>
</div>
<!-- AUDIOBOOK DETAIL -->
<div class="page" id="page-abs-book-detail">
<button class="back-btn" onclick="navigate('audiobooks')">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>Hörbücher
</button>
<div id="abs-book-detail-content"></div>
</div>
<!-- SPOTIFY IMPORTER -->
<div class="page" id="page-spotify-importer">
<div class="page-header">
@@ -498,6 +526,26 @@
</div>
</div>
</div>
<div class="settings-section">
<h3 style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
<span style="width:24px;height:24px;border-radius:6px;background:var(--green-dim);display:inline-flex;align-items:center;justify-content:center;font-size:12px">📚</span>
Audiobookshelf
</h3>
<div class="field"><label>Server URL</label><input id="abs-s-url" type="url" placeholder="https://books.example.com"></div>
<div class="field"><label>Benutzername</label><input id="abs-s-user" type="text" placeholder="admin"></div>
<div class="field"><label>Passwort</label><input id="abs-s-pass" type="password" placeholder="••••••••"></div>
<div id="abs-lib-picker" style="display:none" class="field">
<label>Bibliothek</label>
<select id="abs-lib-select" onchange="absSetLibrary(this.value)" style="width:100%;padding:10px 12px;background:var(--bg2);border:1px solid var(--border2);border-radius:var(--radius-sm);color:var(--text);font-family:var(--font);font-size:14px;outline:none"></select>
</div>
<div class="settings-row" style="margin-top:8px;border-radius:9px">
<span id="abs-s-status" style="font-size:13px;color:var(--green)"></span>
<div style="display:flex;gap:8px">
<button class="btn-outline" style="padding:6px 12px;font-size:12px;color:var(--danger);border-color:rgba(224,85,85,0.3)" onclick="absDisconnect()">Trennen</button>
<button class="btn-outline" id="abs-s-connect-btn" style="padding:6px 12px;font-size:12px" onclick="absConnectSettings()">Verbinden</button>
</div>
</div>
</div>
<div class="settings-section">
<h3 style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
<span style="width:24px;height:24px;border-radius:6px;background:var(--surface2);display:inline-flex;align-items:center;justify-content:center;font-size:12px">🎨</span>
@@ -523,6 +571,123 @@
<div class="toast-container" id="toast-container"></div>
<audio id="audio-element"></audio>
<audio id="preview-audio"></audio>
<audio id="abs-audio"></audio>
<!-- AUDIOBOOKSHELF PLAYER OVERLAY -->
<div id="abs-player" class="abs-player-overlay">
<!-- Header -->
<div class="abs-player-header">
<button class="abs-hbtn" onclick="absClosePlayer()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
</button>
<span class="abs-player-label">Hörbuch</span>
<div style="width:36px"></div>
</div>
<!-- Cover -->
<div class="abs-cover-area">
<div class="abs-cover-wrap">
<img id="abs-cover" src="" alt="" class="abs-cover-img" style="display:none">
<div id="abs-cover-placeholder" class="abs-cover-ph">📚</div>
</div>
</div>
<!-- Info -->
<div class="abs-player-info">
<div id="abs-title" class="abs-player-title">—</div>
<div id="abs-author" class="abs-player-author"></div>
<div id="abs-chapter-name" class="abs-player-chapter"></div>
</div>
<!-- Progress bar -->
<div class="abs-progress-area">
<div class="abs-prog-bar" id="abs-prog-bar"
onmousedown="absProgDown(event)" onmousemove="absProgMove(event)" onmouseup="absProgUp(event)"
ontouchstart="absProgDown(event)" ontouchmove="absProgMove(event)" ontouchend="absProgUp(event)">
<div class="abs-prog-track">
<div class="abs-prog-fill" id="abs-prog-fill"></div>
<div class="abs-prog-thumb" id="abs-prog-thumb"></div>
</div>
</div>
<div class="abs-prog-times">
<span id="abs-time-cur">0:00</span>
<span id="abs-time-rem">-0:00</span>
</div>
</div>
<!-- Main controls -->
<div class="abs-controls">
<button class="abs-ctrl-btn" onclick="absPrevChapter()" title="Voriges Kapitel">
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><polygon points="19 20 9 12 19 4 19 20"/><line x1="5" y1="19" x2="5" y2="5" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/></svg>
</button>
<button class="abs-ctrl-btn abs-skip-btn" onclick="absSkipSecs(-10)" title="-10s">
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10"/>
<polyline points="15 9 12 6 9 9"/>
<path d="M12 6v4"/>
</svg>
<span class="abs-skip-label">10</span>
</button>
<button class="abs-play-pause" id="abs-playpause-btn" onclick="absTogglePlay()">
<svg width="28" height="28" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>
</button>
<button class="abs-ctrl-btn abs-skip-btn" onclick="absSkipSecs(30)" title="+30s">
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<path d="M12 22C17.523 22 22 17.523 22 12S17.523 2 12 2 2 6.477 2 12"/>
<polyline points="9 9 12 6 15 9"/>
<path d="M12 6v4"/>
</svg>
<span class="abs-skip-label">30</span>
</button>
<button class="abs-ctrl-btn" onclick="absNextChapter()" title="Nächstes Kapitel">
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 4 15 12 5 20 5 4"/><line x1="19" y1="5" x2="19" y2="19" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/></svg>
</button>
</div>
<!-- Extra controls -->
<div class="abs-extra-controls">
<!-- Speed -->
<div class="abs-picker-wrap">
<button class="abs-extra-btn" id="abs-speed-btn" onclick="absToggleSpeedPicker()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
<span id="abs-speed-val">1×</span>
</button>
<div id="abs-speed-picker" class="abs-picker">
<button data-speed="0.75" onclick="absSetSpeed(0.75)">0.75×</button>
<button data-speed="1" onclick="absSetSpeed(1)">1×</button>
<button data-speed="1.25" onclick="absSetSpeed(1.25)">1.25×</button>
<button data-speed="1.5" onclick="absSetSpeed(1.5)">1.5×</button>
<button data-speed="1.75" onclick="absSetSpeed(1.75)">1.75×</button>
<button data-speed="2" onclick="absSetSpeed(2)">2×</button>
</div>
</div>
<!-- Sleep timer -->
<div class="abs-picker-wrap">
<button class="abs-extra-btn" id="abs-sleep-btn" onclick="absToggleSleepPicker()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
</button>
<div id="abs-sleep-picker" class="abs-picker abs-picker-right">
<button onclick="absSleepIn(0)">Aus</button>
<button onclick="absSleepIn(5)">5 Min</button>
<button onclick="absSleepIn(10)">10 Min</button>
<button onclick="absSleepIn(15)">15 Min</button>
<button onclick="absSleepIn(30)">30 Min</button>
<button onclick="absSleepIn(45)">45 Min</button>
<button onclick="absSleepIn(60)">60 Min</button>
</div>
</div>
<!-- Chapter list toggle -->
<button class="abs-extra-btn" onclick="absToggleChapters()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>
Kapitel
</button>
</div>
<!-- Chapter list -->
<div id="abs-chapter-list" class="abs-chapter-list"></div>
</div>
<!-- CONTEXT MENU -->
<div class="ctx-menu" id="ctx-menu" style="display:none"></div>