- Support multiple music providers

- feat: Doubleclick on track in Table view to get technical information about it
This commit is contained in:
Kamil
2024-12-03 12:48:27 +00:00
parent 883294d74e
commit 87791cf21d
12 changed files with 306 additions and 109 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@@ -5,14 +5,14 @@ var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
}) })
// Function to open the search modal and trigger the search automatically // Function to open the search modal and trigger the search automatically
function openSearchModal(trackTitle, spotify_id) { function openSearchModal(trackTitle, provider_track_id) {
const modal = new bootstrap.Modal(document.getElementById('searchModal')); const modal = new bootstrap.Modal(document.getElementById('searchModal'));
const searchQueryInput = document.getElementById('search-query'); const searchQueryInput = document.getElementById('search-query');
const spotifyIdInput = document.getElementById('spotify-id'); const providerTrackIdInput = document.getElementById('provider-track-id');
// Pre-fill the input fields // Pre-fill the input fields
searchQueryInput.value = trackTitle; searchQueryInput.value = trackTitle;
spotifyIdInput.value = spotify_id; providerTrackIdInput.value = provider_track_id;
// Show the modal // Show the modal
modal.show(); modal.show();
@@ -85,10 +85,10 @@ function playJellyfinTrack(button, jellyfinId) {
.catch(error => console.error('Error fetching Jellyfin stream URL:', error)); .catch(error => console.error('Error fetching Jellyfin stream URL:', error));
} }
function handleJellyfinClick(event, jellyfinId, trackTitle, spotifyId) { function handleJellyfinClick(event, jellyfinId, trackTitle, providerTrackId) {
if (event.ctrlKey) { if (event.ctrlKey) {
// CTRL key is pressed, open the search modal // CTRL key is pressed, open the search modal
openSearchModal(trackTitle, spotifyId); openSearchModal(trackTitle, providerTrackId);
} else { } else {
// CTRL key is not pressed, play the track // CTRL key is not pressed, play the track
playJellyfinTrack(event.target, jellyfinId); playJellyfinTrack(event.target, jellyfinId);

View File

@@ -34,12 +34,22 @@
<nav> <nav>
<ul class="nav flex-column"> <ul class="nav flex-column">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/"><i class="fab fa-house"></i> Home</a>
</li>
{% for provider in registered_providers %}
<li class="nav-item">
<a class="nav-link " href="/browse?provider={{provider}}">
<i class="fab fa-{{provider.lower()}}"></i> Browse {{provider}}
</a>
</li>
{% endfor %}
<!-- <li class="nav-item">
<a class="nav-link" href="/playlists"><i class="fab fa-spotify"></i> Featured</a> <a class="nav-link" href="/playlists"><i class="fab fa-spotify"></i> Featured</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/categories"><i class="fa-solid fa-layer-group"></i> <a class="nav-link" href="/categories"><i class="fa-solid fa-layer-group"></i>
Categories</a> Categories</a>
</li> </li> -->
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/playlists/monitored"><i <a class="nav-link" href="/playlists/monitored"><i
class="fa-solid fa-tower-observation"></i> Monitored</a> class="fa-solid fa-tower-observation"></i> Monitored</a>
@@ -69,12 +79,22 @@
</div> </div>
<ul class="nav flex-column"> <ul class="nav flex-column">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/"><i class="fas fa-house"></i> Home</a>
</li>
{% for provider in registered_providers %}
<li class="nav-item">
<a class="nav-link " href="/browse?provider={{provider}}">
<i class="fab fa-{{provider.lower()}}"></i> Browse {{provider}}
</a>
</li>
{% endfor %}
<!-- <li class="nav-item">
<a class="nav-link text-white" href="/playlists"><i class="fab fa-spotify"></i> Featured</a> <a class="nav-link text-white" href="/playlists"><i class="fab fa-spotify"></i> Featured</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-white" href="/categories"><i class="fa-solid fa-layer-group"></i> <a class="nav-link text-white" href="/categories"><i class="fa-solid fa-layer-group"></i>
Categories</a> Categories</a>
</li> </li> -->
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-white" href="/playlists/monitored"><i <a class="nav-link text-white" href="/playlists/monitored"><i
class="fa-solid fa-tower-observation"></i> Monitored </a> class="fa-solid fa-tower-observation"></i> Monitored </a>
@@ -108,29 +128,23 @@
<i class="fas fa-bars"></i> <i class="fas fa-bars"></i>
</button> </button>
<h1 class="mb-4 ms-3">{{ title }}</h1> <!-- Search Form -->
<form action="/search" method="GET" class="d-flex flex-grow-1 mb-1 me-2">
<div class="d-flex align-items-center ">
<form action="/search" method="GET" class="w-100">
<div class="input-group">
<input <input
type="search" type="search"
class="form-control" class="form-control me-2"
name="query" name="query"
placeholder="Search Spotify..." placeholder="Search ..."
aria-label="Search" aria-label="Search"
> >
<button class="btn btn-primary" type="submit">Search</button> <button class="btn btn-primary" type="submit">Search</button>
</div>
</form> </form>
<div class="ms-4">
<!-- Display Initials Badge --> <!-- Display Initials Badge -->
<span >{{ session.get('jellyfin_user_name') }}</span> <span>{{ session.get('jellyfin_user_name') }}</span>
</div> </div>
</div> <h1 class="mb-1 ">{{ title }}</h1>
<h3 class="mb-4 ">{{ subtitle }}</h3>
</div>
{% block content %}{% endblock %} {% block content %}{% endblock %}
</div> </div>
</div> </div>

61
templates/browse.html Normal file
View File

@@ -0,0 +1,61 @@
{% extends "base.html" %}
{% block content %}
{% for section in browse_data %}
<div class="browse-section">
<h1>{{ section.title }}</h1>
<div class="row row-cols-1 row-cols-md-4 row-cols-lg-6 g-4">
{% for card in section.items %}
<div class="col">
<div class="card shadow h-100 d-flex flex-column position-relative" >
<a href="/browse/page/{{ card.uri }}?provider={{provider_id}}">
<img src="{{ card.artwork.0.url }}" class="card-img-top" alt="{{ card.title }}">
<div class="card-body d-flex flex-column justify-content-between">
<h5 class="card-title">{{ card.title }}</h5>
</div>
</a>
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
{% endblock %}
<style>
.browse-section {
margin: 20px 0;
}
.browse-section h1 {
font-size: 24px;
margin-bottom: 20px;
}
.browse-cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 20px;
}
.browse-card {
position: relative;
border-radius: 8px;
overflow: hidden;
text-align: center;
color: #fff;
}
.browse-card img {
width: 100%;
height: auto;
display: block;
}
.browse-card .title {
position: absolute;
bottom: 10px;
left: 10px;
font-size: 18px;
text-shadow: 1px 1px 2px #000;
}
</style>

View File

@@ -0,0 +1,12 @@
{% extends "base.html" %}
{% block content %}
<!-- <h1 class="mb-4">{{ data.title }}</h1>
<h6 class="mb-4">{{ data.subtitle }}</h6> -->
<div class="row row-cols-1 row-cols-md-4 row-cols-lg-6 g-4 mt-4" id="items-container">
{% for item in data %}
{% include 'partials/playlist_item.html' %}
{% endfor %}
</div>
{% endblock %}

View File

@@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block content %}
<div class="container-fluid">
{% for provider_id, playlists in provider_playlists_data.items() %}
<div class="provider-section mb-5">
<h2>{{ provider_id }}</h2>
<div class="row row-cols-2 row-cols-md-6 g-4">
{% for item in playlists %}
{% include 'partials/playlist_item.html' %}
{% endfor %}
</div>
</div>
{% endfor %}
</div>
{% endblock %}

View File

@@ -1,5 +1,5 @@
{% if item.can_add %} {% if item.can_add %}
<button class="btn btn-success" hx-post="/addplaylist" hx-include="this" hx-swap="outerHTML" hx-target="this" <button class="btn btn-success" hx-post="/addplaylist?provider={{provider_id}}" hx-include="this" hx-swap="outerHTML" hx-target="this"
data-bs-toggle="tooltip" title="Add to my Jellyfin" data-bs-toggle="tooltip" title="Add to my Jellyfin"
hx-vals='{"item_id": "{{ item.id }}", "item_name": "{{ item.name }}"}'> hx-vals='{"item_id": "{{ item.id }}", "item_name": "{{ item.name }}"}'>
<i class="fa-solid fa-circle-plus"> </i> <i class="fa-solid fa-circle-plus"> </i>

View File

@@ -31,7 +31,7 @@
</button> </button>
</td> </td>
<td> <td>
<button hx-swap="beforebegin" class="btn btn-sm btn-success" hx-post="/associate_track" hx-vals='{"jellyfin_id": "{{ track.Id }}","spotify_id": "{{ spotify_id}}"}' data-bs-toggle="tooltip" title="Link this Track [{{track.Id}}] with Spotify-Track-ID {{ spotify_id }}"> <button hx-swap="beforebegin" class="btn btn-sm btn-success" hx-post="/associate_track" hx-vals='{"jellyfin_id": "{{ track.Id }}","provider_track_id": "{{ provider_track_id}}"}' data-bs-toggle="tooltip" title="Link this Track [{{track.Id}}] with Provider-Track-ID {{ provider_track_id }}">
Link Track Link Track
</button> </button>
</td> </td>

View File

@@ -1,13 +1,13 @@
<div class="d-flex align-items-center row sticky-top py-3 mb-3 bg-dark" style="top: 0; z-index: 1000;"> <div class="d-flex align-items-center row sticky-top py-3 mb-3 bg-dark" style="top: 0; z-index: 1000;">
<div class="col-6"> <div class="col-6">
<img src="{{ playlist_cover }}" class="img-fluid"> <img src="{{ item.image }}" class="img-fluid">
</div> </div>
<div class="col-6"> <div class="col-6">
<div class="playlist-info"> <div class="playlist-info">
<h1>{{ playlist_name }}</h1> <h1>{{ item.name }}</h1>
<p>{{ playlist_description }}</p> <p>{{ item.description }}</p>
<p>{{ track_count }} songs, {{ total_duration }}</p> <p>{{ item.track_count }} songs, {{ total_duration }}</p>
<p>Last Updated: {{ last_updated}} | Last Change: {{ last_changed}}</p> <p>Last Updated: {{ item.last_updated}} | Last Change: {{ item.last_changed}}</p>
{% include 'partials/_add_remove_button.html' %} {% include 'partials/_add_remove_button.html' %}
</div> </div>
</div> </div>

View File

@@ -5,7 +5,7 @@
<th scope="col">Title</th> <th scope="col">Title</th>
<th scope="col">Artist</th> <th scope="col">Artist</th>
<th scope="col">Duration</th> <th scope="col">Duration</th>
<th scope="col">Spotify</th> <th scope="col">{{provider_id}}</th>
<th scope="col">Preview</th> <th scope="col">Preview</th>
<th scope="col">Status</th> <th scope="col">Status</th>
<th scope="col">Jellyfin</th> <th scope="col">Jellyfin</th>
@@ -13,19 +13,22 @@
</thead> </thead>
<tbody> <tbody>
{% for track in tracks %} {% for track in tracks %}
<tr> <tr hx-get="/track_details/{{track.provider_track_id}}?provider={{ provider_id }}"
hx-target="#trackDetailsModalcontent" hx-trigger="dblclick" hx-on="htmx:afterOnLoad:showModal">
<th scope="row">{{ loop.index }}</th> <th scope="row">{{ loop.index }}</th>
<td>{{ track.title }}</td> <td>{{ track.title }}</td>
<td>{{ track.artist }}</td> <td>{{ track.artist }}</td>
<td>{{ track.duration }}</td> <td>{{ track.duration }}</td>
<td> <td>
<a href="{{ track.url }}" target="_blank" class="text-success" data-bs-toggle="tooltip" title="Open in Spotify"> <a href="{{ track.url[0] }}" target="_blank" class="text-success" data-bs-toggle="tooltip"
<i class="fab fa-spotify fa-lg"></i> title="Open in {{ track.provider_id }}">
<i class="fab fa-{{ track.provider_id.lower() }} fa-lg"></i>
</a> </a>
</td> </td>
<td> <td>
{% if track.preview_url %} {% if track.preview_url %}
<button class="btn btn-sm btn-primary" onclick="playPreview(this, '{{ track.preview_url }}')" data-bs-toggle="tooltip" title="Play Preview"> <button class="btn btn-sm btn-primary" onclick="playPreview(this, '{{ track.preview_url }}')"
data-bs-toggle="tooltip" title="Play Preview">
<i class="fas fa-play"></i> <i class="fas fa-play"></i>
</button> </button>
{% else %} {% else %}
@@ -36,13 +39,13 @@
</td> </td>
<td> <td>
{% if not track.downloaded %} {% if not track.downloaded %}
<button class="btn btn-sm btn-danger" <button class="btn btn-sm btn-danger" data-bs-toggle="tooltip"
data-bs-toggle="tooltip" title="{{ track.download_status if track.download_status else 'Not downloaded'}}"> title="{{ track.download_status if track.download_status else 'Not downloaded'}}">
<i class="fa-solid fa-triangle-exclamation"></i> <i class="fa-solid fa-triangle-exclamation"></i>
</button> </button>
{% else %} {% else %}
<button class="btn btn-sm btn-success" data-bs-toggle="tooltip" title="Track downloaded"> <button class="btn btn-sm btn-success" data-bs-toggle="tooltip" title="Downloaded">
<i class="fa-solid fa-circle-check"></i> <i class="fa-solid fa-check"></i>
</button> </button>
{% endif %} {% endif %}
</td> </td>
@@ -50,26 +53,46 @@
{% set title = track.title | replace("'","") %} {% set title = track.title | replace("'","") %}
{% if track.jellyfin_id %} {% if track.jellyfin_id %}
<button class="btn btn-sm btn-success" onclick="handleJellyfinClick(event, '{{ track.jellyfin_id }}', '{{ title }}', '{{ track.spotify_id }}')" data-bs-toggle="tooltip" title="Play from Jellyfin (Hold CTRL Key to reassing a new track)"> <button class="btn btn-sm btn-success"
onclick="handleJellyfinClick(event, '{{ track.jellyfin_id }}', '{{ title }}', '{{ track.provider_track_id }}')"
data-bs-toggle="tooltip" title="Play from Jellyfin (Hold CTRL Key to reassing a new track)">
<i class="fas fa-play"></i> <i class="fas fa-play"></i>
</button> </button>
{% elif track.downloaded %} {% elif track.downloaded %}
<span data-bs-toggle="tooltip" title="Track Downloaded, but not in Jellyfin or could not be associated automatically. You can try to do the association manually"> <span data-bs-toggle="tooltip"
<button class="btn btn-sm btn-warning" onclick="openSearchModal('{{ title }}','{{track.spotify_id}}')"> title="Track Downloaded, but not in Jellyfin or could not be associated automatically. You can try to do the association manually">
<button class="btn btn-sm btn-warning"
onclick="openSearchModal('{{ title }}','{{track.provider_track_id}}')">
<i class="fas fa-triangle-exclamation"></i> <i class="fas fa-triangle-exclamation"></i>
</button> </button>
</span> </span>
{% else %} {% else %}
<span data-bs-toggle="tooltip" title="Not Available"> <span>
<button class="btn btn-sm" disabled><i class="fas fa-ban"></i></button> <button class="btn btn-sm" onclick="openSearchModal('{{ title }}','{{track.provider_track_id}}')"
data-bs-toggle="tooltip" title="Hold CTRL Key to reassing a new track"><i class="fas fa-ban"></i></button>
</span> </span>
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<div class="modal fade" id="trackDetailsModal" tabindex="-1" aria-labelledby="trackDetailsModalLabel"
aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content" id="trackDetailsModalcontent">
</div>
</div>
</div> </div>
<script>
document.addEventListener('htmx:afterOnLoad', function(event) {
if (event.detail.target.id === 'trackDetailsModalcontent') {
const modal = new bootstrap.Modal(document.getElementById('trackDetailsModal'));
modal.show();
}
});
</script>
<div class="modal fade" id="searchModal" tabindex="-1" aria-labelledby="searchModalLabel" aria-hidden="true"> <div class="modal fade" id="searchModal" tabindex="-1" aria-labelledby="searchModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable"> <div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content"> <div class="modal-content">
@@ -81,8 +104,9 @@
<!-- htmx-enabled form --> <!-- htmx-enabled form -->
<form id="search-form" hx-get="/search_jellyfin" hx-target="#search-results" hx-trigger="submit"> <form id="search-form" hx-get="/search_jellyfin" hx-target="#search-results" hx-trigger="submit">
<div class="input-group mb-3"> <div class="input-group mb-3">
<input type="text" class="form-control" id="search-query" name="search_query" placeholder="Search for a track..."> <input type="text" class="form-control" id="search-query" name="search_query"
<input type="hidden" class="form-control" id="spotify-id" name="spotify_id" > placeholder="Search for a track...">
<input type="hidden" class="form-control" id="provider-track-id" name="provider_track_id">
<button class="btn btn-primary" type="submit">Search</button> <button class="btn btn-primary" type="submit">Search</button>
</div> </div>
</form> </form>
@@ -98,5 +122,3 @@
</div> </div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,49 @@
<div class="col" id="item-id-{{ item.id }}">
<div class="card shadow h-100 d-flex flex-column position-relative">
<!-- Badge: Only show if status is available (i.e., playlist has been requested) -->
{% if item.status %}
<span style="z-index: 99;" class="badge position-absolute top-0 end-0 m-2
{% if item.status == 'green' %} bg-success
{% elif item.status == 'yellow' %} bg-warning text-dark
{% else %} bg-danger {% endif %}" data-bs-toggle="tooltip" title="{% if item.track_count > 0 %}
{{ item.tracks_available }} Track Available / {{ item.tracks_linked}} Tracks linked/ {{ item.track_count}} Total
{%endif%}
">
{% if item.track_count > 0 %}
{{ item.tracks_available }} / {{ item.tracks_linked}} / {{ item.track_count}}
{% else %}
not Available
{% endif %}
</span>
{% endif %}
<!-- Card Image -->
<div style="position: relative;">
<img src="{{ item.image }}" class="card-img-top" alt="{{ item.name }}">
</div>
<!-- Card Body -->
<div class="card-body d-flex flex-column justify-content-between">
<div>
<h5 class="card-title">{{ item.name }}</h5>
<p class="card-text">{{ item.description }}</p>
</div>
<div class="mt-auto pt-3">
{% if item.type == 'category'%}
<a href="{{ item.url }}" class="btn btn-primary" data-bs-toggle="tooltip" title="View Playlist">
<i class="fa-solid fa-eye"></i>
</a>
{%else%}
<a href="/playlist/view/{{ item.id }}?provider={{provider_id}}" class="btn btn-primary" data-bs-toggle="tooltip"
title="View Playlist details">
<i class="fa-solid fa-eye"></i>
</a>
{%endif%}
{% include 'partials/_add_remove_button.html' %}
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,23 @@
<div class="modal-header">
<h5 class="modal-title" id="trackDetailsModalLabel">Track Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p><strong>Title:</strong> {{ track.title }}</p>
<p><strong>Artist:</strong> {{ track.artist }}</p>
<p><strong>Duration:</strong> {{ track.duration }}</p>
<p><strong>Duration (ms):</strong> {{ track.duration_ms }}</p>
<p><strong>Provider:</strong> {{ track.provider_id }}</p>
<p><strong>Provider Track URL:</strong> <a href="{{track.provider_track_url}}" target="_blank">{{track.provider_track_url}}</a></p>
<!-- <p><strong>Preview URL:</strong> <a href="{{ track.preview_url }}" target="_blank">{{ track.preview_url if track.preview_url else 'No Preview Available' }}</a></p> -->
<p><strong>Status:</strong> {{ 'Downloaded' if track.downloaded else 'Not Downloaded' }}</p>
<p><strong>Jellyfin ID:</strong> {{ track.jellyfin_id | jellyfin_link }}</p>
<p><strong>Provider Track ID:</strong> {{ track.provider_track_id }}</p>
<p><strong>Download Status:</strong> {{ track.download_status }}</p>
<p><strong>Filesystem Path:</strong> {{ track.filesystem_path }}</p>
<p><strong>Jellyfin Filesystem Path:</strong> {{ track.jellyfin_filesystem_path if track.jellyfin_filesystem_path else 'N/A' }}</p>
<p>{{ track.jellyfin_filesystem_path | audioprofile(track.jellyfin_filesystem_path) if track.jellyfin_filesystem_path else 'N/A' }}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>