mirror of
https://github.com/rembo10/headphones.git
synced 2026-09-10 00:32:52 +01:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f43e8258d | ||
|
|
d667cdb5c9 | ||
|
|
c459eb1074 | ||
|
|
b039492072 | ||
|
|
0ac76fbebc |
+125
-239
@@ -5,6 +5,7 @@
|
|||||||
%>
|
%>
|
||||||
|
|
||||||
<%def name="headerIncludes()">
|
<%def name="headerIncludes()">
|
||||||
|
|
||||||
<div id="subhead_container">
|
<div id="subhead_container">
|
||||||
<div id="back_to_previous_link">
|
<div id="back_to_previous_link">
|
||||||
<a href="artistPage?ArtistID=${album['ArtistID']}" class="back">« Back to ${album['ArtistName']}</a>
|
<a href="artistPage?ArtistID=${album['ArtistID']}" class="back">« Back to ${album['ArtistName']}</a>
|
||||||
@@ -12,18 +13,16 @@
|
|||||||
<div id="subhead_menu">
|
<div id="subhead_menu">
|
||||||
|
|
||||||
<a id="menu_link_delete" href="deleteAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}"><i class="fa fa-trash-o"></i> Delete Album</a>
|
<a id="menu_link_delete" href="deleteAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}"><i class="fa fa-trash-o"></i> Delete Album</a>
|
||||||
|
|
||||||
%if album['Status'] == 'Skipped' or album['Status'] == 'Ignored':
|
%if album['Status'] == 'Skipped' or album['Status'] == 'Ignored':
|
||||||
<a id="menu_link_wanted" href="#" class="album-action" data-action="queueAlbum" data-album-id="${album['AlbumID']}" data-artist-id="${album['ArtistID']}" data-new="False" data-success-msg="'${album['AlbumTitle']}' added to queue"><i class="fa fa-heart"></i> Mark Album as Wanted</a>
|
<a id="menu_link_wanted" href="javascript:void(0)" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}&new=False', $(this),true)" data-success="'${album['AlbumTitle']}' added to queue"><i class="fa fa-heart"></i> Mark Album as Wanted</a>
|
||||||
%elif album['Status'] == 'Wanted':
|
%elif album['Status'] == 'Wanted':
|
||||||
<a id="menu_link_check" href="#" class="album-action" data-action="queueAlbum" data-album-id="${album['AlbumID']}" data-artist-id="${album['ArtistID']}" data-new="True" data-success-msg="Forced checking successful"><i class="fa fa-search"></i> Force Check</a>
|
<a id="menu_link_check" href="javascript:void(0)" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}&new=True', $(this));" data-success="Forced checking successful"><i class="fa fa-search"></i> Force Check</a>
|
||||||
<a id="menu_link_skipped" href="#" class="album-action" data-action="unqueueAlbum" data-album-id="${album['AlbumID']}" data-artist-id="${album['ArtistID']}" data-success-msg="'${album['AlbumTitle']}' marked as Skipped"><i class="fa fa-step-forward"></i> Mark Album as Skipped</a>
|
<a id="menu_link_skipped" href="javascript:void(0)" onclick="doAjaxCall('unqueueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}', $(this),true);" data-success="'${album['AlbumTitle']}' marked as Skipped"><i class="fa fa-step-forward"></i> Mark Album as Skipped</a>
|
||||||
%else:
|
%else:
|
||||||
<a id="menu_link_retry" href="#" class="album-action" data-action="queueAlbum" data-album-id="${album['AlbumID']}" data-artist-id="${album['ArtistID']}" data-new="False" data-success-msg="Retrying the same version of '${album['AlbumTitle']}'"><i class="fa fa-refresh"></i> Retry Download</a>
|
<a id="menu_link_retry" href="javascript:void(0)" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}&new=False', $(this),true);" data-success="Retrying the same version of '${album['AlbumTitle']}'"><i class="fa fa-refresh"></i> Retry Download</a>
|
||||||
<a id="menu_link_new" href="#" class="album-action" data-action="queueAlbum" data-album-id="${album['AlbumID']}" data-artist-id="${album['ArtistID']}" data-new="True" data-success-msg="Looking for a new version of '${album['AlbumTitle']}'"><i class="fa fa-download"></i> Try New Version</a>
|
<a id="menu_link_new" href="javascript:void(0)" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}&new=True', $(this),true);" data-success="Looking for a new version of '${album['AlbumTitle']}'"><i class="fa fa-download"></i> Try New Version</a>
|
||||||
%endif
|
%endif
|
||||||
|
<a class="menu_link_edit" id="album_chooser" href="javascript:void(0)"><i class="fa fa-pencil"></i> Choose Alternate Release</a>
|
||||||
<a class="menu_link_edit dialog-trigger" id="album_chooser" href="#" data-dialog-id="dialog"><i class="fa fa-pencil"></i> Choose Alternate Release</a>
|
|
||||||
<div id="dialog" title="Choose an Alternate Release" style="display:none" class="configtable">
|
<div id="dialog" title="Choose an Alternate Release" style="display:none" class="configtable">
|
||||||
<div class="links">
|
<div class="links">
|
||||||
<%
|
<%
|
||||||
@@ -31,7 +30,7 @@
|
|||||||
%>
|
%>
|
||||||
%if not alternate_albums:
|
%if not alternate_albums:
|
||||||
<p>No alternate releases found. Try refreshing the artist (if the artist is being refreshed, please wait until it's finished)</p>
|
<p>No alternate releases found. Try refreshing the artist (if the artist is being refreshed, please wait until it's finished)</p>
|
||||||
<h2><a id="refresh_artist_btn" href="#" class="album-action" data-action="refreshArtist" data-artist-id="${album['ArtistID']}" data-success-msg="'${album['ArtistName']}' is being refreshed">Refresh Artist</a></h2>
|
<h2><a id="refresh_artist" onclick="doAjaxCall('refreshArtist?ArtistID=${album['ArtistID']}', $(this)), true" href="javascript:void(0)" data-success="'${album['ArtistName']}' is being refreshed">Refresh Artist</a></h2>
|
||||||
%else:
|
%else:
|
||||||
%for alternate_album in alternate_albums:
|
%for alternate_album in alternate_albums:
|
||||||
<%
|
<%
|
||||||
@@ -44,15 +43,14 @@
|
|||||||
alternate_album_name = alternate_album['AlbumTitle'] + " (" + alternate_album['ReleaseCountry'] + ", " + str(alternate_album['ReleaseDate']) + ", " + alternate_album['ReleaseFormat'] + ") [" + str(have_track_count) + "/" + str(track_count) + " tracks]"
|
alternate_album_name = alternate_album['AlbumTitle'] + " (" + alternate_album['ReleaseCountry'] + ", " + str(alternate_album['ReleaseDate']) + ", " + alternate_album['ReleaseFormat'] + ") [" + str(have_track_count) + "/" + str(track_count) + " tracks]"
|
||||||
|
|
||||||
%>
|
%>
|
||||||
<a href="#" class="album-action alternate-release-switch" data-action="switchAlbum" data-album-id="${album['AlbumID']}" data-release-id="${alternate_album['ReleaseID']}" data-success-msg="Switched release to: ${alternate_album_name}">${alternate_album_name}</a><a href="${mb_link}" target="_blank" class="external-link">MB</a><br>
|
<a href="javascript:void(0)" onclick="doAjaxCall('switchAlbum?AlbumID=${album['AlbumID']}&ReleaseID=${alternate_album['ReleaseID']}', $(this), 'table');" data-success="Switched release to: ${alternate_album_name}">${alternate_album_name}</a><a href="${mb_link}" target="_blank">MB</a><br>
|
||||||
%endfor
|
%endfor
|
||||||
%endif
|
%endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<a class="menu_link_edit" id="edit_search_term" href="javascript:void(0)"><i class="fa fa-pencil"></i> Edit Search Term</a>
|
||||||
<a class="menu_link_edit dialog-trigger" id="edit_search_term" href="#" data-dialog-id="dialog2"><i class="fa fa-pencil"></i> Edit Search Term</a>
|
|
||||||
<div id="dialog2" title="Enter your own search term for this album" style="display:none" class="configtable">
|
<div id="dialog2" title="Enter your own search term for this album" style="display:none" class="configtable">
|
||||||
<form action="editSearchTerm" method="GET" id="editSearchTermForm">
|
<form action="editSearchTerm" method="GET" id="editSearchTerm">
|
||||||
<input type="hidden" name="AlbumID" value="${album['AlbumID']}">
|
<input type="hidden" name="AlbumID" value="${album['AlbumID']}">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<%
|
<%
|
||||||
@@ -63,11 +61,10 @@
|
|||||||
%>
|
%>
|
||||||
<input type="text" value="${search_term}" name="SearchTerm" size="40" />
|
<input type="text" value="${search_term}" name="SearchTerm" size="40" />
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="album-action-submit" data-action="editSearchTerm" data-success-msg="Search term updated">Save changes</button>
|
<input type="button" value="Save changes" onclick="doAjaxCall('editSearchTerm',$(this),'tabs',true);return false;" data-success="Search term updated"/>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<a class="menu_link_edit" id="choose_specific_download" href="javascript:void(0)" onclick="getAvailableDownloads()"><i class="fa fa-search"></i> Choose Specific Download</a>
|
||||||
<a class="menu_link_edit dialog-trigger" id="choose_specific_download" href="#" data-dialog-id="choose_specific_download_dialog" data-action="getAvailableDownloads"><i class="fa fa-search"></i> Choose Specific Download</a>
|
|
||||||
<div id="choose_specific_download_dialog" title="Choose a specific download for this album" style="display:none" class="configtable">
|
<div id="choose_specific_download_dialog" title="Choose a specific download for this album" style="display:none" class="configtable">
|
||||||
<table class="display" id="downloads_table">
|
<table class="display" id="downloads_table">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -91,7 +88,7 @@
|
|||||||
<div class="table_wrapper">
|
<div class="table_wrapper">
|
||||||
<div id="albumheader" class="clearfix">
|
<div id="albumheader" class="clearfix">
|
||||||
<div id="albumImg">
|
<div id="albumImg">
|
||||||
<img alt="${album['AlbumTitle']} album art" class="albumArt" src="artwork/album/${album['AlbumID']}">
|
<img height="200" alt="" class="albumArt" src="artwork/album/${album['AlbumID']}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 id="albumname">
|
<h1 id="albumname">
|
||||||
@@ -109,6 +106,7 @@
|
|||||||
albumduration = helpers.convert_milliseconds(totalduration)
|
albumduration = helpers.convert_milliseconds(totalduration)
|
||||||
except:
|
except:
|
||||||
albumduration = 'n/a'
|
albumduration = 'n/a'
|
||||||
|
|
||||||
%>
|
%>
|
||||||
<div class="albuminfo">
|
<div class="albuminfo">
|
||||||
<div id="albumInfo"></div>
|
<div id="albumInfo"></div>
|
||||||
@@ -189,290 +187,178 @@
|
|||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="headIncludes()">
|
<%def name="headIncludes()">
|
||||||
${parent.headIncludes()} <%-- Ensure parent head includes are kept --%>
|
|
||||||
<link rel="stylesheet" href="interfaces/default/css/data_table.css">
|
<link rel="stylesheet" href="interfaces/default/css/data_table.css">
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="javascriptIncludes()">
|
<%def name="javascriptIncludes()">
|
||||||
${parent.javascriptIncludes()} <%-- Ensure parent javascript includes are kept --%>
|
|
||||||
<script src="js/libs/jquery.dataTables.min.js"></script>
|
<script src="js/libs/jquery.dataTables.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Define a global object for page-specific functions to avoid polluting global scope directly
|
function getAlbumInfo() {
|
||||||
var AlbumPage = AlbumPage || {};
|
|
||||||
|
|
||||||
AlbumPage.search_results = []; // Moved to page-specific scope
|
|
||||||
|
|
||||||
AlbumPage.getAlbumInfo = function() {
|
|
||||||
var id = "${album['AlbumID']}";
|
var id = "${album['AlbumID']}";
|
||||||
var elem = $("#albumInfo");
|
var elem = $("#albumInfo");
|
||||||
// Assuming getInfo is defined in common.js
|
|
||||||
if (typeof getInfo === 'function') {
|
|
||||||
getInfo(elem,id,'album');
|
getInfo(elem,id,'album');
|
||||||
} else {
|
|
||||||
console.warn("getInfo function not found. Album info might not be loaded.");
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
AlbumPage.initDialogs = function() {
|
function initThisPage() {
|
||||||
// General handler for opening dialogs based on data-dialog-id
|
$('#album_chooser').click(function() {
|
||||||
$(document).on('click', '.dialog-trigger', function(e) {
|
$('#dialog').dialog({
|
||||||
e.preventDefault();
|
|
||||||
var dialogId = $(this).data('dialog-id');
|
|
||||||
var $dialog = $('#' + dialogId);
|
|
||||||
|
|
||||||
if ($dialog.length) {
|
|
||||||
// Specific logic for choose_specific_download_dialog before opening
|
|
||||||
if (dialogId === 'choose_specific_download_dialog') {
|
|
||||||
AlbumPage.getAvailableDownloads();
|
|
||||||
} else {
|
|
||||||
$dialog.dialog({
|
|
||||||
width: 500,
|
width: 500,
|
||||||
maxHeight: 500,
|
maxHeight: 500
|
||||||
// Add close event to clear dynamic content if any
|
|
||||||
close: function() {
|
|
||||||
if (dialogId === 'downloads_table_body') {
|
|
||||||
$('#downloads_table_body').empty(); // Clear table content on close
|
|
||||||
// Re-initialize DataTable if needed on next open, or destroy it here
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
return false;
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
$('#edit_search_term').click(function() {
|
||||||
|
$('#dialog2').dialog({
|
||||||
|
width: 500,
|
||||||
|
maxHeight: 500
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
$('#refresh_artist').click(function() {
|
||||||
|
$('#dialog').dialog("close");
|
||||||
|
});
|
||||||
|
initActions();
|
||||||
|
setTimeout(function(){
|
||||||
|
initFancybox();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
// Specific handler for refreshing artist from alternate releases dialog
|
$('#track_table').dataTable({
|
||||||
$(document).on('click', '#refresh_artist_btn', function(e) {
|
"aaSorting": [],
|
||||||
e.preventDefault();
|
"bFilter": false,
|
||||||
var $this = $(this);
|
"bInfo": false,
|
||||||
// Assuming doAjaxCall is defined in common.js
|
"bPaginate": false,
|
||||||
if (typeof doAjaxCall === 'function') {
|
"bDestroy": true
|
||||||
doAjaxCall($this.data('action') + '?ArtistID=' + $this.data('artist-id'), $this, true, $this.data('success-msg'));
|
|
||||||
}
|
|
||||||
$('#dialog').dialog("close"); // Close dialog after action
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
AlbumPage.initDataTables = function() {
|
function getAvailableDownloads() {
|
||||||
// Initialize track_table
|
ShowSpinner();
|
||||||
$('#track_table').DataTable({ // Use .DataTable() for new versions of DataTables
|
$.getJSON("choose_specific_download?AlbumID=${album['AlbumID']}", function(data) {
|
||||||
"ordering": false, // aaSorting -> ordering
|
loader.remove();
|
||||||
"searching": false, // bFilter -> searching
|
feedback.fadeOut();
|
||||||
"info": false, // bInfo -> info
|
search_results = data
|
||||||
"paging": false, // bPaginate -> paging
|
|
||||||
"destroy": true // bDestroy -> destroy
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
AlbumPage.getAvailableDownloads = function() {
|
|
||||||
AlbumPage.ShowSpinner();
|
|
||||||
var albumId = "${album['AlbumID']}";
|
|
||||||
$.getJSON("choose_specific_download?AlbumID=" + albumId, function(data) {
|
|
||||||
AlbumPage.loader.remove(); // Assuming loader is attached to AlbumPage scope now
|
|
||||||
AlbumPage.feedback.fadeOut(); // Assuming feedback is attached to AlbumPage scope now
|
|
||||||
AlbumPage.search_results = data; // Store results
|
|
||||||
|
|
||||||
// Clear previous content
|
|
||||||
$('#downloads_table_body').empty();
|
|
||||||
|
|
||||||
for( var i = 0, len = data.length; i < len; i++ ) {
|
for( var i = 0, len = data.length; i < len; i++ ) {
|
||||||
$('#downloads_table_body').append(
|
$('#downloads_table_body').append('<tr><td id="title"><a href="javascript:void(0)" onclick="downloadSpecificRelease('+i+')">'+data[i].title+'</a></td><td id="size"><span title='+data[i].size+'></span>'+(data[i].size / (1024*1024)).toFixed(2)+' MB</td><td id="provider">'+data[i].provider+'</td><td id="kind">'+data[i].kind+'</td><td id="matches">'+data[i].matches+'</td></tr>');
|
||||||
'<tr>' +
|
|
||||||
'<td id="title"><a href="#" class="download-specific-release-link" data-index="' + i + '">' + data[i].title + '</a></td>' +
|
|
||||||
'<td id="size"><span title="'+data[i].size+'"></span>' + (data[i].size / (1024*1024)).toFixed(2) + ' MB</td>' +
|
|
||||||
'<td id="provider">' + data[i].provider + '</td>' +
|
|
||||||
'<td id="kind">' + data[i].kind + '</td>' +
|
|
||||||
'<td id="matches">' + data[i].matches + '</td>' +
|
|
||||||
'</tr>'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
$('#downloads_table').dataTable({
|
||||||
// Destroy and re-initialize the DataTable for downloads
|
"aoColumns": [
|
||||||
if ($.fn.DataTable.isDataTable('#downloads_table')) {
|
null,
|
||||||
$('#downloads_table').DataTable().destroy();
|
{"sType": "title-numeric"},
|
||||||
}
|
null,
|
||||||
$('#downloads_table').DataTable({
|
null,
|
||||||
"columnDefs": [ // aoColumns -> columnDefs
|
{"sType": "string"}
|
||||||
{ "orderable": false, "targets": [0, 2, 3] }, // Disable ordering for Title, Provider, Kind
|
|
||||||
{ "type": "title-numeric", "targets": 1 }, // sType -> type
|
|
||||||
{ "type": "string", "targets": 4 }
|
|
||||||
],
|
],
|
||||||
"order": [[ 4, 'desc']], // aaSorting -> order
|
"aaSorting": [[ 4, 'desc']],
|
||||||
"searching": false,
|
"bFilter": false,
|
||||||
"info": false,
|
"bInfo": false,
|
||||||
"paging": false,
|
"bPaginate": false,
|
||||||
"destroy": true // Important for re-initialization
|
"bDestroy": true
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#choose_specific_download_dialog").dialog({
|
$("#choose_specific_download_dialog").dialog({
|
||||||
width: "80%",
|
width: "80%",
|
||||||
maxHeight: 500,
|
maxHeight: 500
|
||||||
modal: true // Added modal to make it more common practice
|
|
||||||
});
|
});
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
AlbumPage.downloadSpecificRelease = function(i){
|
|
||||||
var release = AlbumPage.search_results[i]; // Get from stored results
|
|
||||||
|
|
||||||
var url = "download_specific_release?AlbumID=${album['AlbumID']}" +
|
|
||||||
"&title=" + encodeURIComponent(release.title) +
|
|
||||||
"&size=" + release.size +
|
|
||||||
"&url=" + encodeURIComponent(release.url) +
|
|
||||||
"&provider=" + encodeURIComponent(release.provider) +
|
|
||||||
"&kind=" + encodeURIComponent(release.kind);
|
|
||||||
|
|
||||||
AlbumPage.ShowSpinner();
|
|
||||||
$.getJSON(url, function(data) {
|
|
||||||
AlbumPage.loader.remove();
|
|
||||||
AlbumPage.feedback.fadeOut();
|
|
||||||
// Assuming refreshSubmenu is defined globally or in common.js
|
|
||||||
if (typeof refreshSubmenu === 'function') {
|
|
||||||
refreshSubmenu();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function downloadSpecificRelease(i){
|
||||||
|
|
||||||
|
title = search_results[i].title
|
||||||
|
size = search_results[i].size
|
||||||
|
url = search_results[i].url
|
||||||
|
provider = search_results[i].provider
|
||||||
|
kind = search_results[i].kind
|
||||||
|
|
||||||
|
ShowSpinner();
|
||||||
|
$.getJSON("download_specific_release?AlbumID=${album['AlbumID']}&title="+title+"&size="+size+"&url="+url+"&provider="+provider+"&kind=" + kind, function(data) {
|
||||||
|
loader.remove();
|
||||||
|
feedback.fadeOut();
|
||||||
|
refreshSubmenu();
|
||||||
$("#choose_specific_download_dialog").dialog("close");
|
$("#choose_specific_download_dialog").dialog("close");
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
AlbumPage.ShowSpinner = function() {
|
function ShowSpinner() {
|
||||||
AlbumPage.feedback = $("#ajaxMsg"); // Assign to AlbumPage scope
|
feedback = $("#ajaxMsg");
|
||||||
var update = $("#updatebar");
|
update = $("#updatebar");
|
||||||
if ( update.is(":visible") ) {
|
if ( update.is(":visible") ) {
|
||||||
var height = update.height() + 35;
|
var height = update.height() + 35;
|
||||||
AlbumPage.feedback.css("bottom",height + "px");
|
feedback.css("bottom",height + "px");
|
||||||
} else {
|
} else {
|
||||||
AlbumPage.feedback.removeAttr("style");
|
feedback.removeAttr("style");
|
||||||
|
}
|
||||||
|
loader = $("<i class='fa fa-refresh fa-spin'></i>");
|
||||||
|
feedback.prepend(loader);
|
||||||
|
feedback.fadeIn();
|
||||||
}
|
}
|
||||||
AlbumPage.loader = $("<i class='fa fa-refresh fa-spin'></i>"); // Assign to AlbumPage scope
|
|
||||||
AlbumPage.feedback.prepend(AlbumPage.loader);
|
|
||||||
AlbumPage.feedback.fadeIn();
|
|
||||||
};
|
|
||||||
|
|
||||||
AlbumPage.loadingMessage = false;
|
var loadingMessage = false;
|
||||||
AlbumPage.spinner_active = false;
|
var spinner_active = false;
|
||||||
AlbumPage.loadingtext_active = false;
|
var loadingtext_active = false;
|
||||||
AlbumPage.refreshInterval = null; // Initialize as null
|
var refreshInterval;
|
||||||
AlbumPage.wasLoading = false;
|
var wasLoading = false;
|
||||||
AlbumPage.x = 0;
|
var x = 0;
|
||||||
|
|
||||||
AlbumPage.checkAlbumStatus = function() {
|
function checkAlbumStatus() {
|
||||||
$.getJSON("getAlbumjson?AlbumID=${album['AlbumID']}", function(data) {
|
$.getJSON("getAlbumjson?AlbumID=${album['AlbumID']}", function(data) {
|
||||||
if (data['Status'] === "Loading"){
|
if (data['Status'] == "Loading"){
|
||||||
AlbumPage.wasLoading = true;
|
wasLoading = true;
|
||||||
$('#albumnamelink').text(data["AlbumTitle"]);
|
$('#albumnamelink').text(data["AlbumTitle"]);
|
||||||
$('#artistnamelink').text(data["ArtistName"]);
|
$('#artistnamelink').text(data["ArtistName"]);
|
||||||
if (AlbumPage.loadingMessage === false){
|
if (loadingMessage == false){
|
||||||
$("#ajaxMsg").after( "<div id='ajaxMsg2' class='ajaxMsg'></div>" );
|
$("#ajaxMsg").after( "<div id='ajaxMsg2' class='ajaxMsg'></div>" );
|
||||||
// Assuming showArtistMsg is defined globally or in common.js
|
|
||||||
if (typeof showArtistMsg === 'function') {
|
|
||||||
showArtistMsg("Getting album information");
|
showArtistMsg("Getting album information");
|
||||||
|
loadingMessage = true;
|
||||||
}
|
}
|
||||||
AlbumPage.loadingMessage = true;
|
if (spinner_active == false){
|
||||||
|
$('#albumname').prepend('<i class="fa fa-refresh fa-spin" id="albumnamespinner"></i>')
|
||||||
|
spinner_active = true;
|
||||||
}
|
}
|
||||||
if (AlbumPage.spinner_active === false){
|
if (loadingtext_active == false){
|
||||||
$('#albumname').prepend('<i class="fa fa-refresh fa-spin" id="albumnamespinner"></i>');
|
$('#albumname').append('<h3 id="loadingtext"><i>(Album information is currently being loaded)</i></h3>')
|
||||||
AlbumPage.spinner_active = true;
|
loadingtext_active = true;
|
||||||
}
|
}
|
||||||
if (AlbumPage.loadingtext_active === false){
|
|
||||||
$('#albumname').append('<h3 id="loadingtext"><i>(Album information is currently being loaded)</i></h3>');
|
|
||||||
AlbumPage.loadingtext_active = true;
|
|
||||||
}
|
}
|
||||||
} else {
|
else{
|
||||||
AlbumPage.x++;
|
if (++x === 10) {
|
||||||
if (AlbumPage.x === 10 || AlbumPage.wasLoading || $("#artistname").text().trim() === "Loading") { // Combined conditions
|
clearInterval(refreshInterval);
|
||||||
if (AlbumPage.refreshInterval) { // Clear only if interval is set
|
|
||||||
clearInterval(AlbumPage.refreshInterval);
|
|
||||||
}
|
}
|
||||||
location.reload(); // Reload the page to show updated status
|
var sts = $("#artistname").text().trim();
|
||||||
|
if (wasLoading == true || sts == "Loading"){
|
||||||
|
location.reload();
|
||||||
|
$('#albumnamespinner').remove()
|
||||||
|
$('#loadingtext').remove()
|
||||||
|
$('#ajaxMsg2').remove()
|
||||||
|
spinner_active = false
|
||||||
|
loadingtext_active = false
|
||||||
|
loadingMessage = false
|
||||||
}
|
}
|
||||||
$('#albumnamespinner').remove();
|
|
||||||
$('#loadingtext').remove();
|
|
||||||
$('#ajaxMsg2').remove();
|
|
||||||
AlbumPage.spinner_active = false;
|
|
||||||
AlbumPage.loadingtext_active = false;
|
|
||||||
AlbumPage.loadingMessage = false;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
// jQuery DataTables custom sorting
|
|
||||||
jQuery.extend( jQuery.fn.dataTableExt.oSort, {
|
jQuery.extend( jQuery.fn.dataTableExt.oSort, {
|
||||||
"title-numeric-pre": function ( a ) {
|
"title-numeric-pre": function ( a ) {
|
||||||
// Ensure it handles cases where title attribute might be missing or different
|
var x = a.match(/title="*(-?[0-9\.]+)/)[1];
|
||||||
var match = a.match(/title="*(-?[0-9\.]+)/);
|
return parseFloat( x );
|
||||||
return match ? parseFloat( match[1] ) : -Infinity; // Return a safe default
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"title-numeric-asc": function ( a, b ) {
|
"title-numeric-asc": function ( a, b ) {
|
||||||
return ((a < b) ? -1 : ((a > b) ? 1 : 0));
|
return ((a < b) ? -1 : ((a > b) ? 1 : 0));
|
||||||
},
|
},
|
||||||
|
|
||||||
"title-numeric-desc": function ( a, b ) {
|
"title-numeric-desc": function ( a, b ) {
|
||||||
return ((a < b) ? 1 : ((a > b) ? -1 : 0));
|
return ((a < b) ? 1 : ((a > b) ? -1 : 0));
|
||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
AlbumPage.getAlbumInfo();
|
getAlbumInfo();
|
||||||
AlbumPage.initDialogs(); // Initialize dialog triggers
|
initThisPage();
|
||||||
AlbumPage.initDataTables(); // Initialize DataTables
|
checkAlbumStatus();
|
||||||
// Do not use initActions() here unless it's strictly needed and modernized itself.
|
refreshInterval = setInterval(function(){
|
||||||
// setTimeout for fancybox is likely not needed if elements are ready or use delegated events.
|
checkAlbumStatus();
|
||||||
// If fancybox is for dynamic content, initialize it after content is loaded.
|
|
||||||
// initFancybox(); // Re-evaluate if this is still needed or how it's used.
|
|
||||||
|
|
||||||
// Event handler for album actions
|
|
||||||
// Delegated to #subhead_menu as these links are within it
|
|
||||||
$('#subhead_menu').on('click', '.album-action', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
var $this = $(this);
|
|
||||||
var action = $this.data('action');
|
|
||||||
var albumId = $this.data('album-id');
|
|
||||||
var artistId = $this.data('artist-id');
|
|
||||||
var newStatus = $this.data('new'); // Boolean 'true' or 'false'
|
|
||||||
var releaseId = $this.data('release-id');
|
|
||||||
var successMsg = $this.data('success-msg');
|
|
||||||
|
|
||||||
var url = action + '?AlbumID=' + albumId;
|
|
||||||
if (artistId) url += '&ArtistID=' + artistId;
|
|
||||||
if (newStatus !== undefined) url += '&new=' + newStatus;
|
|
||||||
if (releaseId) url += '&ReleaseID=' + releaseId;
|
|
||||||
|
|
||||||
// Assuming doAjaxCall is defined globally or in common.js
|
|
||||||
if (typeof doAjaxCall === 'function') {
|
|
||||||
doAjaxCall(url, $this, true, successMsg);
|
|
||||||
} else {
|
|
||||||
console.error("doAjaxCall function is not defined!");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Event handler for specific download links within the dialog
|
|
||||||
// Delegated to #downloads_table_body as these links are dynamically added
|
|
||||||
$('#downloads_table_body').on('click', '.download-specific-release-link', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
var index = $(this).data('index');
|
|
||||||
AlbumPage.downloadSpecificRelease(index);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Event handler for form submission within dialogs
|
|
||||||
// Delegated to the specific form's ID
|
|
||||||
$('#editSearchTermForm').on('submit', function(e) {
|
|
||||||
e.preventDefault(); // Prevent default form submission
|
|
||||||
var $this = $(this);
|
|
||||||
var action = $this.attr('action'); // Get action from form
|
|
||||||
var formData = $this.serialize(); // Serialize form data
|
|
||||||
var successMsg = $this.find('.album-action-submit').data('success-msg');
|
|
||||||
|
|
||||||
// Assuming doAjaxCall is defined globally or in common.js
|
|
||||||
if (typeof doAjaxCall === 'function') {
|
|
||||||
// Pass form data instead of element, specify 'form' type if common.js handles it
|
|
||||||
doAjaxCall(action + '?' + formData, $this.find('.album-action-submit'), true, successMsg);
|
|
||||||
}
|
|
||||||
$('#dialog2').dialog("close"); // Close dialog after submission
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// Start checking album status periodically
|
|
||||||
AlbumPage.checkAlbumStatus();
|
|
||||||
AlbumPage.refreshInterval = setInterval(function(){
|
|
||||||
AlbumPage.checkAlbumStatus();
|
|
||||||
}, 3000);
|
}, 3000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+132
-272
@@ -8,33 +8,29 @@
|
|||||||
<%def name="headerIncludes()">
|
<%def name="headerIncludes()">
|
||||||
<div id="subhead_container">
|
<div id="subhead_container">
|
||||||
<div id="subhead_menu">
|
<div id="subhead_menu">
|
||||||
<a id="menu_link_refresh" href="#" class="artist-action" data-action="refreshArtist" data-artist-id="${artist['ArtistID']}" data-success-msg="${artist['ArtistName']} refreshed"><i class="fa fa-refresh"></i> Refresh Artist</a>
|
<a id="menu_link_refresh" onclick="doSimpleAjaxCall('refreshArtist?ArtistID=${artist['ArtistID']}')" href="javascript:void(0)"><i class="fa fa-refresh"></i> Refresh Artist</a>
|
||||||
<a id="menu_link_delete" href="deleteArtist?ArtistID=${artist['ArtistID']}"><i class="fa fa-trash-o"></i> Delete Artist</a>
|
<a id="menu_link_delete" href="deleteArtist?ArtistID=${artist['ArtistID']}"><i class="fa fa-trash-o"></i> Delete Artist</a>
|
||||||
<a id="menu_link_scan" href="#" class="artist-action" data-action="scanArtist" data-artist-id="${artist['ArtistID']}" data-success-msg="'${artist['ArtistName']}' was scanned"><i class="fa fa-refresh"></i> Scan Artist</a>
|
<a id="menu_link_scan" onclick="doAjaxCall('scanArtist?ArtistID=${artist['ArtistID']}', $(this)),'table'" href="javascript:void(0)" data-success="'${artist['ArtistName']}' was scanned"><i class="fa fa-refresh"></i> Scan Artist</a>
|
||||||
|
|
||||||
%if artist['Status'] == 'Paused':
|
%if artist['Status'] == 'Paused':
|
||||||
<a id="menu_link_resume" href="#" class="artist-action" data-action="resumeArtist" data-artist-id="${artist['ArtistID']}" data-success-msg="${artist['ArtistName']} resumed"><i class="fa fa-play"></i> Resume Artist</a>
|
<a id="menu_link_resume" href="javascript:void(0)" onclick="doAjaxCall('resumeArtist?ArtistID=${artist['ArtistID']}',$(this),true)" data-success="${artist['ArtistName']} resumed"><i class="fa fa-play"></i> Resume Artist</a>
|
||||||
%else:
|
%else:
|
||||||
<a id="menu_link_pauze" href="#" class="artist-action" data-action="pauseArtist" data-artist-id="${artist['ArtistID']}" data-success-msg="${artist['ArtistName']} paused"><i class="fa fa-pause"></i> Pause Artist</a>
|
<a id="menu_link_pauze" href="javascript:void(0)" onclick="doAjaxCall('pauseArtist?ArtistID=${artist['ArtistID']}',$(this),true)" data-success="${artist['ArtistName']} paused"><i class="fa fa-pause"></i> Pause Artist</a>
|
||||||
%endif
|
%endif
|
||||||
|
|
||||||
%if artist['IncludeExtras']:
|
%if artist['IncludeExtras']:
|
||||||
<a id="menu_link_removeextra" href="#" class="artist-action" data-action="removeExtras" data-artist-id="${artist['ArtistID']}" data-artist-name="${artist['ArtistName']}" data-success-msg="Extras removed for ${artist['ArtistName']}"><i class="fa fa-minus"></i> Remove Extras</a>
|
<a id="menu_link_removeextra" href="javascript:void(0)" onclick="doAjaxCall('removeExtras?ArtistID=${artist['ArtistID']}&ArtistName=${artist['ArtistName']}',$(this),'submenu&table')" data-success="Extras removed for ${artist['ArtistName']}"><i class="fa fa-minus"></i> Remove Extras</a>
|
||||||
<a class="menu_link_edit dialog-trigger" id="menu_link_modifyextra" href="#" data-dialog-id="dialog"><i class="fa fa-pencil"></i> Modify Extras</a>
|
<a class="menu_link_edit" id="menu_link_modifyextra" href="javascript:void(0)"><i class="fa fa-pencil"></i> Modify Extras</a>
|
||||||
%else:
|
%else:
|
||||||
<a id="menu_link_getextra" href="#" class="dialog-trigger" data-dialog-id="dialog"><i class="fa fa-plus"></i> Get Extras</a>
|
<a id="menu_link_getextra" href="javascript:void(0)"><i class="fa fa-plus"></i> Get Extras</a>
|
||||||
%endif
|
%endif
|
||||||
|
|
||||||
<div id="dialog" title="Choose Which Extras to Fetch" style="display:none" class="configtable">
|
<div id="dialog" title="Choose Which Extras to Fetch" style="display:none" class="configtable">
|
||||||
<form action="getExtras" method="get" id="getExtrasForm">
|
<form action="getExtras" method="get" class="form">
|
||||||
<input type="hidden" name="ArtistID" value="${artist['ArtistID']}">
|
<input type="hidden" name="ArtistID" value="${artist['ArtistID']}">
|
||||||
<input type="hidden" name="newstyle" value="true">
|
<input type="hidden" name="newstyle" value="true">
|
||||||
%for extra in extras:
|
%for extra in extras:
|
||||||
<input type="checkbox" id="extra_${extra}" name="${extra}" value="1" ${extras[extra]} />
|
<input type="checkbox" id="${extra}" name="${extra}" value="1" ${extras[extra]} />${string.capwords(extra)}<br>
|
||||||
<label for="extra_${extra}">${string.capwords(extra)}</label><br>
|
|
||||||
%endfor
|
%endfor
|
||||||
<br>
|
<br>
|
||||||
<button type="submit">Fetch Extras</button>
|
<input id="submit" type="submit" value="Fetch Extras">
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -45,7 +41,7 @@
|
|||||||
<%def name="body()">
|
<%def name="body()">
|
||||||
<div id="artistheader" class="clearfix">
|
<div id="artistheader" class="clearfix">
|
||||||
<div id="artistImg">
|
<div id="artistImg">
|
||||||
<img id="artistImage" class="albumArt" alt="${artist['ArtistName']} artist image" src="artwork/artist/${artist['ArtistID']}"/>
|
<img id="artistImage" class="albumArt" alt="" src="artwork/artist/${artist['ArtistID']}"/>
|
||||||
</div>
|
</div>
|
||||||
<h1 id="artistname">
|
<h1 id="artistname">
|
||||||
<a href="http://musicbrainz.org/artist/${artist['ArtistID']}" id="artistnamelink">${artist['ArtistName']}</a>
|
<a href="http://musicbrainz.org/artist/${artist['ArtistID']}" id="artistnamelink">${artist['ArtistName']}</a>
|
||||||
@@ -53,10 +49,10 @@
|
|||||||
<div id="artistBio"></div>
|
<div id="artistBio"></div>
|
||||||
</div>
|
</div>
|
||||||
<ul id="artistCalendar" style="display:none;"></ul>
|
<ul id="artistCalendar" style="display:none;"></ul>
|
||||||
<form action="markAlbums" method="get" id="markAlbumsForm">
|
<form action="markAlbums" method="get" id="markAlbums">
|
||||||
<input type="hidden" name="ArtistID" value=${artist['ArtistID']}>
|
<input type="hidden" name="ArtistID" value=${artist['ArtistID']}>
|
||||||
<div id="markalbum">Mark selected albums as
|
<div id="markalbum">Mark selected albums as
|
||||||
<select name="action" id="markAlbumsActionSelect">
|
<select name="action" onChange="doAjaxCall('markAlbums',$(this),'table',true);" data-error="You didn't select any albums">
|
||||||
<option disabled="disabled" selected="selected">Choose...</option>
|
<option disabled="disabled" selected="selected">Choose...</option>
|
||||||
<option value="Wanted">Wanted</option>
|
<option value="Wanted">Wanted</option>
|
||||||
<option value="WantedNew">Wanted (new only)</option>
|
<option value="WantedNew">Wanted (new only)</option>
|
||||||
@@ -64,12 +60,12 @@
|
|||||||
<option value="Ignored">Ignored</option>
|
<option value="Ignored">Ignored</option>
|
||||||
<option value="Downloaded">Downloaded</option>
|
<option value="Downloaded">Downloaded</option>
|
||||||
</select>
|
</select>
|
||||||
<button type="submit" style="display:none;"></button> <%-- Hidden submit to allow form submission via JS --%>
|
<input type="hidden" value="Go">
|
||||||
</div>
|
</div>
|
||||||
<table class="display" id="album_table">
|
<table class="display" id="album_table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th id="select"><input type="checkbox" class="select-all-checkbox" aria-label="Select all albums" /></th>
|
<th id="select"><input type="checkbox" onClick="toggle(this)" /></th>
|
||||||
<th id="albumart"></th>
|
<th id="albumart"></th>
|
||||||
<th id="albumname">Name</th>
|
<th id="albumname">Name</th>
|
||||||
<th id="reldate">Date</th>
|
<th id="reldate">Date</th>
|
||||||
@@ -125,24 +121,24 @@
|
|||||||
|
|
||||||
%>
|
%>
|
||||||
<tr class="grade${grade}">
|
<tr class="grade${grade}">
|
||||||
<td id="select"><input type="checkbox" name="${album['AlbumID']}" class="checkbox album-checkbox" /></td>
|
<td id="select"><input type="checkbox" name="${album['AlbumID']}" class="checkbox" /></td>
|
||||||
<td id="albumart"><img class="albumArtnostretch" id="thumb_${album['AlbumID']}" alt="${album['AlbumTitle']} thumbnail" src="artwork/thumbs/album/${album['AlbumID']}"></td>
|
<td id="albumart"><img class="albumArtnostretch" id="${album['AlbumID']}" src="artwork/thumbs/album/${album['AlbumID']}" height="64" width="64"></td>
|
||||||
<td id="albumname"><a href="albumPage?AlbumID=${album['AlbumID']}">${album['AlbumTitle']}</a></td>
|
<td id="albumname"><a href="albumPage?AlbumID=${album['AlbumID']}">${album['AlbumTitle']}</a></td>
|
||||||
<td id="reldate">${album['ReleaseDate']}</td>
|
<td id="reldate">${album['ReleaseDate']}</td>
|
||||||
<td id="type">${album['Type']}</td>
|
<td id="type">${album['Type']}</td>
|
||||||
<td id="score">${album['CriticScore']}/${album['UserScore']}</td>
|
<td id="score">${album['CriticScore']}/${album['UserScore']}</td>
|
||||||
<td id="status">${album['Status']}
|
<td id="status">${album['Status']}
|
||||||
%if album['Status'] == 'Skipped' or album['Status'] == 'Ignored':
|
%if album['Status'] == 'Skipped' or album['Status'] == 'Ignored':
|
||||||
[<a href="#" class="album-action-inline" data-action="queueAlbum" data-album-id="${album['AlbumID']}" data-artist-id="${album['ArtistID']}" data-success-msg="'${album['AlbumTitle']}' added to Wanted list">want</a>]
|
[<a href="javascript:void(0)" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}',$(this),'table')" data-success="'${album['AlbumTitle']}' added to Wanted list">want</a>]
|
||||||
%elif (album['Status'] == 'Wanted' or album['Status'] == 'Wanted Lossless'):
|
%elif (album['Status'] == 'Wanted' or album['Status'] == 'Wanted Lossless'):
|
||||||
[<a href="#" class="album-action-inline" data-action="unqueueAlbum" data-album-id="${album['AlbumID']}" data-artist-id="${album['ArtistID']}" data-success-msg="'${album['AlbumTitle']}' skipped">skip</a>] [<a href="#" class="album-action-inline" data-action="queueAlbum" data-album-id="${album['AlbumID']}" data-artist-id="${album['ArtistID']}" data-success-msg="Trying to download'${album['AlbumTitle']}'" title="Search if available for download">search</a>]
|
[<a href="javascript:void(0)" onclick="doAjaxCall('unqueueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}',$(this),'table')" data-success="'${album['AlbumTitle']}' skipped">skip</a>] [<a href="javascript:void(0)" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}', $(this),'table')" data-success="Trying to download'${album['AlbumTitle']}'" title="Search if available for download">search</a>]
|
||||||
%else:
|
%else:
|
||||||
[<a href="#" class="album-action-inline" data-action="queueAlbum" data-album-id="${album['AlbumID']}" data-artist-id="${album['ArtistID']}" data-success-msg="Retrying the same version of '${album['AlbumTitle']}'" title="Retry the same download again">retry</a>][<a href="#" class="album-action-inline" data-action="queueAlbum" data-album-id="${album['AlbumID']}" data-artist-id="${album['ArtistID']}" data-new="True" title="Try a new download, skipping all previously tried nzbs" data-success-msg="Looking for a new version of '${album['AlbumTitle']}'">new</a>]
|
[<a href="javascript:void(0)" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}', $(this),'table')" data-success="Retrying the same version of '${album['AlbumTitle']}'" title="Retry the same download again">retry</a>][<a href="javascript:void(0)" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}&new=True', $(this),'table')" title="Try a new download, skipping all previously tried nzbs" data-success="Downloading new version for '${album['AlbumTitle']}'" data-success="Looking for a new version of '${album['AlbumTitle']}'">new</a>]
|
||||||
%endif
|
%endif
|
||||||
%if albumformat in lossy_formats and album['Status'] == 'Skipped':
|
%if albumformat in lossy_formats and album['Status'] == 'Skipped':
|
||||||
[<a id="wantlossless" href="#" class="album-action-inline" data-action="queueAlbum" data-album-id="${album['AlbumID']}" data-artist-id="${album['ArtistID']}" data-lossless="True" data-success-msg="Lossless version of '${album['AlbumTitle']}' added to queue">want lossless</a>]
|
[<a id="wantlossless" href="javascript:void(0)" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}&lossless=True', $(this),'table')" data-success="Lossless version of '${album['AlbumTitle']}' added to queue">want lossless</a>]
|
||||||
%elif albumformat in lossy_formats and (album['Status'] == 'Snatched' or album['Status'] == 'Downloaded'):
|
%elif albumformat in lossy_formats and (album['Status'] == 'Snatched' or album['Status'] == 'Downloaded'):
|
||||||
[<a id="wantlossless" href="#" class="album-action-inline" data-action="queueAlbum" data-album-id="${album['AlbumID']}" data-artist-id="${album['ArtistID']}" data-lossless="True" data-success-msg="Retrying the same lossless version of '${album['AlbumTitle']}'">retry lossless</a>]
|
[<a id="wantlossless" href="javascript:void(0)" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}&lossless=True', $(this),'table')" data-success="Retrying the same lossless version of '${album['AlbumTitle']}'">retry lossless</a>]
|
||||||
%endif
|
%endif
|
||||||
</td>
|
</td>
|
||||||
<td id="have"><span title="${percent}"><span><div class="progress-container"><div style="width:${percent}%"><div class="havetracks">${havetracks}/${totaltracks}</div></div></div></td>
|
<td id="have"><span title="${percent}"><span><div class="progress-container"><div style="width:${percent}%"><div class="havetracks">${havetracks}/${totaltracks}</div></div></div></td>
|
||||||
@@ -156,313 +152,177 @@
|
|||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="headIncludes()">
|
<%def name="headIncludes()">
|
||||||
${parent.headIncludes()} <%-- Ensure parent head includes are kept --%>
|
|
||||||
<link rel="stylesheet" href="interfaces/default/css/data_table.css">
|
<link rel="stylesheet" href="interfaces/default/css/data_table.css">
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="javascriptIncludes()">
|
<%def name="javascriptIncludes()">
|
||||||
${parent.javascriptIncludes()} <%-- Ensure parent javascript includes are kept --%>
|
|
||||||
<script src="js/libs/jquery.dataTables.min.js"></script>
|
<script src="js/libs/jquery.dataTables.min.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Define a global object for page-specific functions to avoid polluting global scope directly
|
function getArtistBio() {
|
||||||
var ArtistPage = ArtistPage || {};
|
|
||||||
|
|
||||||
ArtistPage.getArtistBio = function() {
|
|
||||||
var id = "${artist['ArtistID']}";
|
var id = "${artist['ArtistID']}";
|
||||||
var elem = $("#artistBio");
|
var elem = $("#artistBio");
|
||||||
// Assuming getInfo is defined in common.js
|
|
||||||
if (typeof getInfo === 'function') {
|
|
||||||
getInfo(elem,id,'artist');
|
getInfo(elem,id,'artist');
|
||||||
} else {
|
|
||||||
console.warn("getInfo function not found. Artist bio might not be loaded.");
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
ArtistPage.initDialogs = function() {
|
<%
|
||||||
// General handler for opening dialogs based on data-dialog-id
|
if headphones.CONFIG.SONGKICK_FILTER_ENABLED:
|
||||||
$(document).on('click', '.dialog-trigger', function(e) {
|
songkick_filter_enabled = "true"
|
||||||
e.preventDefault();
|
else:
|
||||||
var dialogId = $(this).data('dialog-id');
|
songkick_filter_enabled = "false"
|
||||||
var $dialog = $('#' + dialogId);
|
|
||||||
|
|
||||||
if ($dialog.length) {
|
if not headphones.CONFIG.SONGKICK_LOCATION:
|
||||||
$dialog.dialog({
|
songkick_location = "none"
|
||||||
width: 500,
|
else:
|
||||||
maxHeight: 500,
|
songkick_location = headphones.CONFIG.SONGKICK_LOCATION
|
||||||
modal: true // Added modal for better UX
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Submit handler for getExtrasForm
|
if headphones.CONFIG.SONGKICK_ENABLED:
|
||||||
$('#getExtrasForm').on('submit', function(e) {
|
songkick_enabled = "true"
|
||||||
e.preventDefault();
|
else:
|
||||||
var $this = $(this);
|
songkick_enabled = "false"
|
||||||
var action = $this.attr('action');
|
|
||||||
var formData = $this.serialize();
|
|
||||||
var artistID = $this.find('input[name="ArtistID"]').val(); // Get ArtistID
|
|
||||||
var successMsg = "Extras fetch initiated for " + "${artist['ArtistName']}"; // Dynamic success message
|
|
||||||
|
|
||||||
if (typeof doAjaxCall === 'function') {
|
%>
|
||||||
doAjaxCall(action + '?' + formData, $this, true, successMsg);
|
function getArtistsCalendar() {
|
||||||
} else {
|
|
||||||
console.error("doAjaxCall function is not defined!");
|
|
||||||
}
|
|
||||||
$('#dialog').dialog("close");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
ArtistPage.getArtistsCalendar = function() {
|
|
||||||
var template, calendarDomNode;
|
var template, calendarDomNode;
|
||||||
|
|
||||||
calendarDomNode = $("#artistCalendar");
|
calendarDomNode = $("#artistCalendar");
|
||||||
|
|
||||||
template = '<li><a target="_blank" href="URI"><span class="sk-name">NAME</span><span class="sk-location">LOC</span></a></li>';
|
template = '<li><a target="_blank" href="URI"><span class="sk-name">NAME</span><span class="sk-location">LOC</span></a></li>';
|
||||||
|
|
||||||
// Python variables are injected directly into this JS block
|
$.getJSON("https://api.songkick.com/api/3.0/artists/mbid:${artist['ArtistID']}/calendar.json?apikey=${headphones.CONFIG.SONGKICK_APIKEY}&jsoncallback=?",
|
||||||
var songkick_filter_enabled = ${'true' if headphones.CONFIG.SONGKICK_FILTER_ENABLED else 'false'};
|
|
||||||
var songkick_location = ${'"{}"'.format(headphones.CONFIG.SONGKICK_LOCATION) if headphones.CONFIG.SONGKICK_LOCATION else 'null'};
|
|
||||||
var songkick_enabled = ${'true' if headphones.CONFIG.SONGKICK_ENABLED else 'false'};
|
|
||||||
var songkick_apikey = "${headphones.CONFIG.SONGKICK_APIKEY}";
|
|
||||||
|
|
||||||
if (!songkick_enabled || !songkick_apikey) {
|
|
||||||
console.log("Songkick is not enabled or API key is missing.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$.getJSON("https://api.songkick.com/api/3.0/artists/mbid:${artist['ArtistID']}/calendar.json?apikey=" + songkick_apikey + "&jsoncallback=?",
|
|
||||||
function(data){
|
function(data){
|
||||||
if (data['resultsPage'] && data['resultsPage'].totalEntries >= 1 && data['resultsPage'].results && data['resultsPage'].results.event) {
|
if (data['resultsPage'].totalEntries >= 1) {
|
||||||
var events = data.resultsPage.results.event;
|
if (${songkick_filter_enabled}) {
|
||||||
if (songkick_filter_enabled && songkick_location) {
|
data.resultsPage.results.event = $.grep(data.resultsPage.results.event, function(element,index){
|
||||||
events = $.grep(events, function(element,index){
|
return element.venue.metroArea.id == ${songkick_location};
|
||||||
return element.venue && element.venue.metroArea && element.venue.metroArea.id == songkick_location;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (events.length > 0) {
|
if (data.resultsPage.results.event.length > 0) {
|
||||||
|
var tourDate;
|
||||||
|
|
||||||
calendarDomNode.show();
|
calendarDomNode.show();
|
||||||
$("#artistImg").addClass('on-tour');
|
$("#artistImg").addClass('on-tour');
|
||||||
|
|
||||||
$.each(events, function(i, event) {
|
jQuery.each(data.resultsPage.results.event, function(i, event) {
|
||||||
var tourDate = template;
|
tourDate = template;
|
||||||
tourDate = tourDate.replace('URI', event.uri || '#');
|
tourDate = tourDate.replace('URI',event.uri);
|
||||||
tourDate = tourDate.replace('NAME', event.displayName || 'N/A');
|
tourDate = tourDate.replace('NAME',event.displayName);
|
||||||
tourDate = tourDate.replace('LOC', (event.location && event.location.city) ? event.location.city : 'N/A');
|
tourDate = tourDate.replace('LOC',event.location.city);
|
||||||
|
|
||||||
calendarDomNode.append(tourDate);
|
calendarDomNode.append(tourDate);
|
||||||
});
|
});
|
||||||
|
|
||||||
calendarDomNode.append('<li><img src="interfaces/default/images/songkick.png" alt="concerts by songkick" class="sk-logo" /></li>');
|
calendarDomNode.append('<li><img src="interfaces/default/images/songkick.png" alt="concerts by songkick" class="sk-logo" /></li>');
|
||||||
|
|
||||||
// Handle "More..." button logic for calendar
|
$(function() {
|
||||||
calendarDomNode.each(function() {
|
$("#artistCalendar").each(function() {
|
||||||
$("li:gt(4)", this).hide(); /* :gt() is zero-indexed */
|
$("li:gt(4)", this).hide(); /* :gt() is zero-indexed */
|
||||||
$("li:nth-child(5)", this).after("<br><li class='more'><a href='#'>More...</a></li>"); /* :nth-child() is one-indexed */
|
$("li:nth-child(5)", this).after("<br><li class='more'><a href='#'>More...</a></li>"); /* :nth-child() is one-indexed */
|
||||||
});
|
});
|
||||||
$("li.more").on("click", 'a', function(e) {
|
$("li.more").on("click", 'a', function() {
|
||||||
e.preventDefault(); // Prevent default link behavior
|
|
||||||
var li = $(this).parents("li:first");
|
var li = $(this).parents("li:first");
|
||||||
li.parent().children().show();
|
li.parent().children().show();
|
||||||
li.remove();
|
li.remove();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).fail(function(jqXHR, textStatus, errorThrown) {
|
}
|
||||||
console.error("Songkick API call failed: ", textStatus, errorThrown);
|
);
|
||||||
});
|
}
|
||||||
};
|
|
||||||
|
|
||||||
ArtistPage.loadingMessage = false;
|
var loadingMessage = false;
|
||||||
ArtistPage.spinner_active = false;
|
var spinner_active = false;
|
||||||
ArtistPage.loadingtext_active = false;
|
var loadingtext_active = false;
|
||||||
ArtistPage.refreshInterval = null; // Initialize as null
|
|
||||||
|
|
||||||
ArtistPage.checkArtistStatus = function() {
|
function checkArtistStatus() {
|
||||||
$.getJSON("getArtistjson?ArtistID=${artist['ArtistID']}", function(data) {
|
$.getJSON("getArtistjson?ArtistID=${artist['ArtistID']}", function(data) {
|
||||||
if (data['Status'] === "Loading"){
|
if (data['Status'] == "Loading"){
|
||||||
// Assuming refreshTable() is defined globally or in common.js and it updates the album_table
|
refreshTable();
|
||||||
if (typeof refreshTable === 'function') {
|
|
||||||
refreshTable(); // Refresh the album table to show loading states if implemented there
|
|
||||||
}
|
|
||||||
|
|
||||||
$('#artistnamelink').text(data["ArtistName"]);
|
$('#artistnamelink').text(data["ArtistName"]);
|
||||||
if (ArtistPage.loadingMessage === false){
|
if (loadingMessage == false){
|
||||||
$("#ajaxMsg").after( "<div id='ajaxMsg2' class='ajaxMsg'></div>" );
|
$("#ajaxMsg").after( "<div id='ajaxMsg2' class='ajaxMsg'></div>" );
|
||||||
if (typeof showArtistMsg === 'function') {
|
|
||||||
showArtistMsg("Getting artist information");
|
showArtistMsg("Getting artist information");
|
||||||
|
loadingMessage = true;
|
||||||
}
|
}
|
||||||
ArtistPage.loadingMessage = true;
|
if (spinner_active == false){
|
||||||
|
$('#artistname').prepend('<i class="fa fa-refresh fa-spin" id="artistnamespinner"></i>')
|
||||||
|
spinner_active = true;
|
||||||
}
|
}
|
||||||
if (ArtistPage.spinner_active === false){
|
if (loadingtext_active == false){
|
||||||
$('#artistname').prepend('<i class="fa fa-refresh fa-spin" id="artistnamespinner"></i>');
|
$('#artistname').append('<h3 id="loadingtext"><i>(Album information for this artist is currently being loaded)</i></h3>')
|
||||||
ArtistPage.spinner_active = true;
|
loadingtext_active = true;
|
||||||
}
|
}
|
||||||
if (ArtistPage.loadingtext_active === false){
|
|
||||||
$('#artistname').append('<h3 id="loadingtext"><i>(Album information for this artist is currently being loaded)</i></h3>');
|
|
||||||
ArtistPage.loadingtext_active = true;
|
|
||||||
}
|
}
|
||||||
} else {
|
else{
|
||||||
// Only reload if previously loading to avoid unnecessary refreshes
|
$('#artistnamespinner').remove()
|
||||||
if (ArtistPage.spinner_active || ArtistPage.loadingtext_active || ArtistPage.loadingMessage) {
|
$('#loadingtext').remove()
|
||||||
location.reload(); // Reload the page to show updated status once loading is complete
|
$('#ajaxMsg2').remove()
|
||||||
|
spinner_active = false
|
||||||
|
loadingtext_active = false
|
||||||
|
loadingMessage = false
|
||||||
}
|
}
|
||||||
$('#artistnamespinner').remove();
|
|
||||||
$('#loadingtext').remove();
|
|
||||||
$('#ajaxMsg2').remove();
|
|
||||||
ArtistPage.spinner_active = false;
|
|
||||||
ArtistPage.loadingtext_active = false;
|
|
||||||
ArtistPage.loadingMessage = false;
|
|
||||||
}
|
|
||||||
}).fail(function(jqXHR, textStatus, errorThrown) {
|
|
||||||
console.error("Error checking artist status: ", textStatus, errorThrown);
|
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
|
function initThisPage() {
|
||||||
|
$('#menu_link_getextra').click(function(event) {
|
||||||
|
$('#dialog').dialog();
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
$('#menu_link_modifyextra').click(function(event) {
|
||||||
|
$('#dialog').dialog();
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
$('#album_table').dataTable({
|
||||||
|
"bDestroy": true,
|
||||||
|
"aoColumns": [
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
{ "sType": "date" },
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
{ "sType": "title-numeric"},
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
],
|
||||||
|
"aoColumnDefs": [
|
||||||
|
{ 'bSortable': false, 'aTargets': [ 0,1 ] }
|
||||||
|
],
|
||||||
|
"bStateSave": true,
|
||||||
|
"oLanguage": {
|
||||||
|
"sLengthMenu":"Show _MENU_ albums per page",
|
||||||
|
"sEmptyTable": "No album information available",
|
||||||
|
"sInfo":"Showing _TOTAL_ albums",
|
||||||
|
"sInfoEmpty":"Showing 0 to 0 of 0 albums",
|
||||||
|
"sInfoFiltered":"(filtered from _MAX_ total albums)",
|
||||||
|
"sSearch": ""},
|
||||||
|
"bPaginate": false,
|
||||||
|
"aaSorting": [[4, 'asc'],[3,'desc']]
|
||||||
|
|
||||||
ArtistPage.initDataTables = function() {
|
|
||||||
$('#album_table').DataTable({
|
|
||||||
"destroy": true, // bDestroy -> destroy
|
|
||||||
"columns": [ // aoColumns -> columns
|
|
||||||
null, null, null,
|
|
||||||
{ "type": "date" }, // sType -> type
|
|
||||||
null, null, null,
|
|
||||||
{ "type": "title-numeric"},
|
|
||||||
null, null
|
|
||||||
],
|
|
||||||
"columnDefs": [ // aoColumnDefs -> columnDefs
|
|
||||||
{ 'orderable': false, 'targets': [ 0, 1 ] } // bSortable -> orderable, aTargets -> targets
|
|
||||||
],
|
|
||||||
"stateSave": true, // bStateSave -> stateSave
|
|
||||||
"language": { // oLanguage -> language
|
|
||||||
"lengthMenu":"Show _MENU_ albums per page",
|
|
||||||
"emptyTable": "No album information available",
|
|
||||||
"info":"Showing _TOTAL_ albums",
|
|
||||||
"infoEmpty":"Showing 0 to 0 of 0 albums",
|
|
||||||
"infoFiltered":"(filtered from _MAX_ total albums)",
|
|
||||||
"search": "" // No need for "Search: " label if using custom icon
|
|
||||||
},
|
|
||||||
"paging": false, // bPaginate -> paging
|
|
||||||
"order": [[4, 'asc'],[3,'desc']] // aaSorting -> order
|
|
||||||
});
|
});
|
||||||
// Assuming resetFilters is defined globally or in common.js
|
|
||||||
if (typeof resetFilters === 'function') {
|
|
||||||
resetFilters("albums");
|
resetFilters("albums");
|
||||||
|
setTimeout(function(){
|
||||||
|
initFancybox();
|
||||||
|
},1500);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
// Init common actions if they are not already handled in base.html document.ready
|
initActions();
|
||||||
// if (typeof initActions === 'function') {
|
initThisPage();
|
||||||
// initActions();
|
getArtistBio();
|
||||||
// }
|
if( ${songkick_enabled} ){
|
||||||
|
getArtistsCalendar();
|
||||||
ArtistPage.getArtistBio();
|
|
||||||
ArtistPage.initDialogs();
|
|
||||||
ArtistPage.initDataTables();
|
|
||||||
|
|
||||||
// Songkick Calendar
|
|
||||||
// The python config values are injected above, so they are available here.
|
|
||||||
if( ${'true' if headphones.CONFIG.SONGKICK_ENABLED else 'false'} ){
|
|
||||||
ArtistPage.getArtistsCalendar();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Artist Status Polling
|
|
||||||
ArtistPage.checkArtistStatus(); // Initial check
|
|
||||||
ArtistPage.refreshInterval = setInterval(function(){
|
|
||||||
ArtistPage.checkArtistStatus();
|
|
||||||
}, 3000); // Increased interval to 3 seconds, was 1.5s which is very frequent
|
|
||||||
|
|
||||||
// Event handler for artist actions in subhead_menu
|
|
||||||
$('#subhead_menu').on('click', '.artist-action', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
var $this = $(this);
|
|
||||||
var action = $this.data('action');
|
|
||||||
var artistId = $this.data('artist-id');
|
|
||||||
var artistName = $this.data('artist-name'); // For removeExtras
|
|
||||||
var successMsg = $this.data('success-msg');
|
|
||||||
|
|
||||||
var url = action + '?ArtistID=' + artistId;
|
|
||||||
if (artistName) url += '&ArtistName=' + encodeURIComponent(artistName);
|
|
||||||
|
|
||||||
if (typeof doAjaxCall === 'function') {
|
|
||||||
doAjaxCall(url, $this, true, successMsg); // 'true' for refreshing content, if common.js supports it
|
|
||||||
} else if (typeof doSimpleAjaxCall === 'function' && action === 'refreshArtist') {
|
|
||||||
// Fallback for refreshArtist if doAjaxCall isn't what's expected for it
|
|
||||||
doSimpleAjaxCall(url);
|
|
||||||
} else {
|
|
||||||
console.error("doAjaxCall or doSimpleAjaxCall function is not defined!");
|
|
||||||
}
|
}
|
||||||
|
checkArtistStatus();
|
||||||
|
setInterval(function(){
|
||||||
|
checkArtistStatus();
|
||||||
|
}, 1500);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Event handler for album actions within the table
|
|
||||||
// Delegated to #album_table body as these links are dynamically added
|
|
||||||
$('#album_table tbody').on('click', '.album-action-inline', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
var $this = $(this);
|
|
||||||
var action = $this.data('action');
|
|
||||||
var albumId = $this.data('album-id');
|
|
||||||
var artistId = $this.data('artist-id');
|
|
||||||
var newStatus = $this.data('new'); // Boolean 'true' or 'false'
|
|
||||||
var lossless = $this.data('lossless'); // Boolean 'true' or 'false'
|
|
||||||
var successMsg = $this.data('success-msg');
|
|
||||||
|
|
||||||
var url = action + '?AlbumID=' + albumId + '&ArtistID=' + artistId;
|
|
||||||
if (newStatus !== undefined) url += '&new=' + newStatus;
|
|
||||||
if (lossless !== undefined) url += '&lossless=' + lossless;
|
|
||||||
|
|
||||||
if (typeof doAjaxCall === 'function') {
|
|
||||||
doAjaxCall(url, $this, 'table', successMsg); // 'table' for refreshing content, if common.js supports it
|
|
||||||
} else {
|
|
||||||
console.error("doAjaxCall function is not defined!");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// "Mark selected albums as" dropdown change
|
|
||||||
$('#markAlbumsActionSelect').on('change', function() {
|
|
||||||
var selectedAction = $(this).val();
|
|
||||||
if (selectedAction) {
|
|
||||||
// Manually trigger form submission with AJAX
|
|
||||||
var $form = $('#markAlbumsForm');
|
|
||||||
var formData = $form.serialize(); // Includes ArtistID and action
|
|
||||||
|
|
||||||
// Get selected album IDs
|
|
||||||
var selectedAlbumIDs = $('.album-checkbox:checked').map(function() {
|
|
||||||
return $(this).attr('name'); // AlbumID is the name attribute
|
|
||||||
}).get();
|
|
||||||
|
|
||||||
if (selectedAlbumIDs.length === 0) {
|
|
||||||
// Show error message as per original data-error
|
|
||||||
if (typeof doAjaxCall === 'function') { // Assuming doAjaxCall can show errors
|
|
||||||
doAjaxCall(null, $(this), null, null, 'You didn\'t select any albums');
|
|
||||||
} else {
|
|
||||||
alert('You didn\'t select any albums');
|
|
||||||
}
|
|
||||||
$(this).val('Choose...'); // Reset dropdown
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct the URL for markAlbums action
|
|
||||||
var url = $form.attr('action') + '?' + formData;
|
|
||||||
// Add selected album IDs as a comma-separated list or multiple parameters
|
|
||||||
$.each(selectedAlbumIDs, function(index, id) {
|
|
||||||
url += '&AlbumID=' + id; // Append each selected AlbumID
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assuming doAjaxCall is defined in common.js
|
|
||||||
if (typeof doAjaxCall === 'function') {
|
|
||||||
// Using null for the element as it's a general form submission
|
|
||||||
doAjaxCall(url, null, 'table', 'Albums marked successfully'); // 'table' for refreshing, success message
|
|
||||||
} else {
|
|
||||||
console.error("doAjaxCall function is not defined!");
|
|
||||||
}
|
|
||||||
$(this).val('Choose...'); // Reset dropdown after action
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// "Select all" checkbox functionality
|
|
||||||
$('.select-all-checkbox').on('change', function() {
|
|
||||||
$('.album-checkbox').prop('checked', $(this).prop('checked'));
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|||||||
@@ -2,37 +2,43 @@
|
|||||||
import headphones
|
import headphones
|
||||||
from headphones import version
|
from headphones import version
|
||||||
%>
|
%>
|
||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<!--[if IE 7 ]> <html lang="en" class="no-js ie7"> <![endif]-->
|
||||||
|
<!--[if IE 8 ]> <html lang="en" class="no-js ie8"> <![endif]-->
|
||||||
|
<!--[if IE 9 ]> <html lang="en" class="no-js ie9"> <![endif]-->
|
||||||
|
<!--[if (gt IE 9)|!(IE)]><!--> <html lang="en" class="no-js"> <!--<![endif]-->
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
|
|
||||||
<title>Headphones - ${title}</title>
|
<title>Headphones - ${title}</title>
|
||||||
<meta name="description" content="Headphones 'default' interface - made by Elmar Kouwenhoven">
|
<meta name="description" content="Headphones 'default' interface - made by Elmar Kouwenhoven">
|
||||||
<meta name="author" content="Elmar Kouwenhoven">
|
<meta name="author" content="Elmar Kouwenhoven">
|
||||||
|
|
||||||
<link rel="icon" href="images/favicon.ico">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
|
<link rel="shortcut icon" href="images/favicon.ico">
|
||||||
<link rel="apple-touch-icon" href="images/headphoneslogo.png">
|
<link rel="apple-touch-icon" href="images/headphoneslogo.png">
|
||||||
|
|
||||||
<link rel="stylesheet" href="css/jquery-ui.min.css">
|
<link rel="stylesheet" href="css/jquery-ui.min.css">
|
||||||
|
|
||||||
<link rel="stylesheet" href="interfaces/default/css/style.css">
|
<link rel="stylesheet" href="interfaces/default/css/style.css">
|
||||||
<link rel="stylesheet" href="interfaces/default/css/font-awesome.min.css">
|
<link rel="stylesheet" href="interfaces/default/css/font-awesome.min.css">
|
||||||
${next.headIncludes()}
|
${next.headIncludes()}
|
||||||
|
|
||||||
|
<script src="js/libs/modernizr-2.8.3.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="container">
|
<div id="container">
|
||||||
<div id="ajaxMsg" class="ajaxMsg"></div>
|
<div id="ajaxMsg" class="ajaxMsg"></div>
|
||||||
|
|
||||||
% if headphones.CONFIG.CHECK_GITHUB and not headphones.CURRENT_VERSION:
|
% if headphones.CONFIG.CHECK_GITHUB and not headphones.CURRENT_VERSION:
|
||||||
<div id="updatebar">
|
<div id="updatebar">
|
||||||
You're running an unknown version of Headphones. <a href="update">Update</a> or
|
You're running an unknown version of Headphones. <a href="update">Update</a> or
|
||||||
<a href="#" class="close-updatebar">Close</a>
|
<a href="javascript:void(0)" onclick="$('#updatebar').slideUp('slow');">Close</a>
|
||||||
</div>
|
</div>
|
||||||
% elif headphones.CONFIG.CHECK_GITHUB and headphones.CURRENT_VERSION != headphones.LATEST_VERSION and headphones.COMMITS_BEHIND > 0 and headphones.INSTALL_TYPE != 'win':
|
% elif headphones.CONFIG.CHECK_GITHUB and headphones.CURRENT_VERSION != headphones.LATEST_VERSION and headphones.COMMITS_BEHIND > 0 and headphones.INSTALL_TYPE != 'win':
|
||||||
<div id="updatebar">
|
<div id="updatebar">
|
||||||
A <a href="https://github.com/${headphones.CONFIG.GIT_USER}/headphones/compare/${headphones.CURRENT_VERSION}...${headphones.LATEST_VERSION}"> newer version</a> is available. You're ${headphones.COMMITS_BEHIND} commits behind. <a href="update">Update</a> or <a href="#" class="close-updatebar">Close</a>
|
A <a href="https://github.com/${headphones.CONFIG.GIT_USER}/headphones/compare/${headphones.CURRENT_VERSION}...${headphones.LATEST_VERSION}"> newer version</a> is available. You're ${headphones.COMMITS_BEHIND} commits behind. <a href="update">Update</a> or <a href="javascript:void(0)" onclick="$('#updatebar').slideUp('slow');">Close</a>
|
||||||
</div>
|
</div>
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
@@ -41,37 +47,36 @@
|
|||||||
<div id="logo">
|
<div id="logo">
|
||||||
<a href="home"><img src="images/headphoneslogo.png" alt="headphones" width="64"></a>
|
<a href="home"><img src="images/headphoneslogo.png" alt="headphones" width="64"></a>
|
||||||
</div>
|
</div>
|
||||||
<nav id="nav">
|
<ul id="nav">
|
||||||
<ul>
|
|
||||||
<li><a href="upcoming">wanted</a></li>
|
<li><a href="upcoming">wanted</a></li>
|
||||||
<li><a href="extras">extras</a></li>
|
<li><a href="extras">extras</a></li>
|
||||||
<li><a href="manage">manage</a></li>
|
<li><a href="manage">manage</a></li>
|
||||||
<li><a href="history">history</a></li>
|
<li><a href="history">history</a></li>
|
||||||
<li><a href="logs">logs</a></li>
|
<li><a href="logs">logs</a></li>
|
||||||
<li><a href="config" class="config" aria-label="Configuration"><i class="fa fa-gear fa-lg"></i></a></li>
|
<li><a href="config" class="config"><i class="fa fa-gear fa-lg"></i></a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
|
||||||
<div id="searchbar">
|
<div id="searchbar">
|
||||||
<form action="search" method="get">
|
<form action="search" method="get">
|
||||||
<input type="text" value="" placeholder="Search" name="name" id="search-input" />
|
<input type="text" value="" placeholder="Search" onfocus="if(this.value==this.defaultValue) this.value='';" name="name" />
|
||||||
<i class='fa fa-search mini-icon'></i>
|
<i class='fa fa-search mini-icon'></i>
|
||||||
<select name="type" id="search_type">
|
<select name="type" id="search_type">
|
||||||
<option value="artist">Artist</option>
|
<option value="artist">Artist</option>
|
||||||
<option value="album">Album</option>
|
<option value="album">Album</option>
|
||||||
<option value="series">Series</option>
|
<option value="series">Series</option>
|
||||||
</select>
|
</select>
|
||||||
<button type="submit">Search</button>
|
<input type="submit" value="Search"/>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main id="main" class="main">
|
<div id="main" class="main">
|
||||||
<div id="subhead">
|
<div id="subhead">
|
||||||
${next.headerIncludes()}
|
${next.headerIncludes()}
|
||||||
</div>
|
</div>
|
||||||
${next.body()}
|
${next.body()}
|
||||||
</main>
|
</div>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<div id="info">
|
<div id="info">
|
||||||
@@ -87,7 +92,7 @@
|
|||||||
<small>
|
<small>
|
||||||
<a href="shutdown"><i class="fa fa-power-off"></i> Shutdown</a> |
|
<a href="shutdown"><i class="fa fa-power-off"></i> Shutdown</a> |
|
||||||
<a href="restart"><i class="fa fa-power-off"></i> Restart</a> |
|
<a href="restart"><i class="fa fa-power-off"></i> Restart</a> |
|
||||||
<a href="#" class="check-update-btn" data-success="Checking for update successful" data-error="Error checking for update"><i class="fa fa-refresh"></i> Check for new version</a>
|
<a href="javascript:void(0)" onclick="doAjaxCall('checkGithub',$(this))" data-success="Checking for update successful" data-error="Error checking for update"><i class="fa fa-refresh"></i> Check for new version</a>
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div id="version">
|
<div id="version">
|
||||||
@@ -100,83 +105,38 @@
|
|||||||
%endif
|
%endif
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<a href="#main" id="toTop" aria-label="Back to top"><i class="fa fa-angle-double-up"></i> <span>Back to top</span></a>
|
<a href="#main" id="toTop"><i class="fa fa-angle-double-up"></i> <span>Back to top</span></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="js/libs/jquery-3.7.1.min.js"></script>
|
<script src="js/libs/jquery-1.11.1.min.js"></script>
|
||||||
<script src="js/libs/jquery-ui.min.js"></script>
|
<script src="js/libs/jquery-ui.min.js"></script>
|
||||||
<script src="js/common.js"></script>
|
<script src="js/common.js"></script>
|
||||||
|
|
||||||
${next.javascriptIncludes()}
|
${next.javascriptIncludes()}
|
||||||
|
|
||||||
<script src="interfaces/default/js/script.js"></script>
|
<script src="interfaces/default/js/script.js"></script>
|
||||||
|
<!-- This template is made by Elmar Kouwenhoven -->
|
||||||
|
|
||||||
|
<!-- Persist search type using local storage -->
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
$(document).ready(function() {
|
|
||||||
// Focus on the first form input that's not hidden
|
|
||||||
$('form:first *:input[type!=hidden]:first').focus();
|
|
||||||
|
|
||||||
// Persist search type using local storage
|
$(document).ready(function() {
|
||||||
|
$('form:first *:input[type!=hidden]:first').focus();
|
||||||
try{
|
try{
|
||||||
var type = window.localStorage.getItem('search_type') || "artist";
|
var type = window.localStorage.getItem('search_type') || "artist";
|
||||||
$("#search_type").val(type);
|
$("#search_type").val(type);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error("Local Storage not available or error accessing it:", e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modernized event listener for closing update bar (delegated)
|
|
||||||
// Uses event delegation for robustness: attaches handler to parent (#container)
|
|
||||||
// which listens for clicks on elements with class .close-updatebar
|
|
||||||
$('#container').on('click', '.close-updatebar', function(e) {
|
|
||||||
e.preventDefault(); // Prevent default link behavior
|
|
||||||
$(this).closest('#updatebar').slideUp('slow');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Modernized event listener for "Check for new version" (delegated)
|
$('select[id=search_type]').change(function() {
|
||||||
$('#actions').on('click', '.check-update-btn', function(e) {
|
var type = $(this).val()
|
||||||
e.preventDefault(); // Prevent default link behavior
|
|
||||||
// Assuming doAjaxCall is defined in common.js or script.js
|
|
||||||
doAjaxCall('checkGithub', $(this));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Event listener for search type change
|
|
||||||
$('select[id=search_type]').on('change', function() {
|
|
||||||
var type = $(this).val();
|
|
||||||
try{
|
try{
|
||||||
window.localStorage.setItem('search_type', type);
|
window.localStorage.setItem('search_type', type);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error("Local Storage not available or error accessing it:", e);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle placeholder text for search input (no longer needs onfocus in HTML)
|
|
||||||
$('#search-input').on('focus', function() {
|
|
||||||
if (this.value === this.defaultValue) {
|
|
||||||
this.value = '';
|
|
||||||
}
|
|
||||||
}).on('blur', function() {
|
|
||||||
if (this.value === '') {
|
|
||||||
this.value = this.defaultValue;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// "Back to top" functionality
|
|
||||||
// Show/hide #toTop button based on scroll position
|
|
||||||
$(window).scroll(function() {
|
|
||||||
if ($(this).scrollTop() > 100) { // Show after scrolling 100px
|
|
||||||
$('#toTop').fadeIn();
|
|
||||||
} else {
|
|
||||||
$('#toTop').fadeOut();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Smooth scroll to top when button is clicked
|
|
||||||
$('#toTop').click(function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
$('html, body').animate({scrollTop : 0}, 800);
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+2396
-714
File diff suppressed because it is too large
Load Diff
+1410
-577
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,4 @@
|
|||||||
<%inherit file="base.html" />
|
<%inherit file="base.html" />
|
||||||
|
|
||||||
<%def name="body()">
|
<%def name="body()">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<h1 class="clearfix"><i class="fa fa-users"></i> Artists You Might Like</h1>
|
<h1 class="clearfix"><i class="fa fa-users"></i> Artists You Might Like</h1>
|
||||||
@@ -8,54 +7,9 @@
|
|||||||
<div class="cloudtag">
|
<div class="cloudtag">
|
||||||
<ul id="cloud">
|
<ul id="cloud">
|
||||||
%for artist in cloudlist:
|
%for artist in cloudlist:
|
||||||
<%--
|
<li><a href="addArtist?artistid=${artist['ArtistID']}" class="tag${artist['Count']}">${artist['ArtistName']}</a></li>
|
||||||
Modified the anchor tag to use data attributes for AJAX
|
|
||||||
and href="#" to prevent default navigation.
|
|
||||||
--%>
|
|
||||||
<li>
|
|
||||||
<a href="#" class="tag${artist['Count']} add-artist-link"
|
|
||||||
data-artist-id="${artist['ArtistID']}"
|
|
||||||
data-artist-name="${artist['ArtistName']}">
|
|
||||||
${artist['ArtistName']}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
%endfor
|
%endfor
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="javascriptIncludes()">
|
|
||||||
${parent.javascriptIncludes()} <%-- Ensure parent javascript includes are kept --%>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
$(document).ready(function() {
|
|
||||||
// Delegated event handler for 'add-artist-link' class
|
|
||||||
$('#cloud').on('click', '.add-artist-link', function(e) {
|
|
||||||
e.preventDefault(); // Prevent the default link behavior (page navigation)
|
|
||||||
|
|
||||||
var $this = $(this);
|
|
||||||
var artistId = $this.data('artist-id');
|
|
||||||
var artistName = $this.data('artist-name'); // Get artist name for feedback
|
|
||||||
|
|
||||||
// Assuming 'doAjaxCall' is available globally (from common.js or similar)
|
|
||||||
// and handles displaying success/error messages.
|
|
||||||
if (typeof doAjaxCall === 'function') {
|
|
||||||
doAjaxCall('addArtist?artistid=' + artistId, $this, 'simple',
|
|
||||||
'Artist "' + artistName + '" added successfully!');
|
|
||||||
|
|
||||||
// Optionally, fade out or remove the artist from the list after adding
|
|
||||||
$this.closest('li').fadeOut(500, function() {
|
|
||||||
$(this).remove();
|
|
||||||
});
|
|
||||||
|
|
||||||
} else {
|
|
||||||
console.error("doAjaxCall function is not defined. Cannot add artist asynchronously.");
|
|
||||||
// Fallback: allow the default link behavior if AJAX is not possible
|
|
||||||
// window.location.href = 'addArtist?artistid=' + artistId;
|
|
||||||
// Or just log an error and do nothing if page reload is not desired.
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</%def>
|
|
||||||
|
|||||||
@@ -7,11 +7,11 @@
|
|||||||
<table class="display" id="artist_table">
|
<table class="display" id="artist_table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="column-albumart"></th> <%-- Changed id to class --%>
|
<th id="albumart"></th>
|
||||||
<th class="column-name">Artist Name</th>
|
<th id="name">Artist Name</th>
|
||||||
<th class="column-status">Status</th>
|
<th id="status">Status</th>
|
||||||
<th class="column-album">Latest Release</th>
|
<th id="album">Latest Release</th>
|
||||||
<th class="column-have">Have</th>
|
<th id="have">Have</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -20,52 +20,14 @@
|
|||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="headIncludes()">
|
<%def name="headIncludes()">
|
||||||
${parent.headIncludes()} <%-- Ensure parent head includes are kept --%>
|
|
||||||
<link rel="stylesheet" href="interfaces/default/css/data_table.css">
|
<link rel="stylesheet" href="interfaces/default/css/data_table.css">
|
||||||
<style>
|
|
||||||
/* Basic CSS for album art thumbnail */
|
|
||||||
.albumArt-thumb {
|
|
||||||
height: 50px;
|
|
||||||
width: 50px;
|
|
||||||
object-fit: cover; /* Ensures image covers the area without distortion */
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Styling for the progress bar */
|
|
||||||
.progress-container {
|
|
||||||
background-color: #eee;
|
|
||||||
border-radius: 5px;
|
|
||||||
height: 15px; /* Adjust height as needed */
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
width: 100%; /* Or a fixed width if preferred */
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-container > div {
|
|
||||||
background-color: #4CAF50; /* Green color for progress */
|
|
||||||
height: 100%;
|
|
||||||
border-radius: 5px;
|
|
||||||
text-align: center;
|
|
||||||
color: white;
|
|
||||||
font-size: 10px; /* Smaller font for percentage */
|
|
||||||
line-height: 15px; /* Vertically align text */
|
|
||||||
}
|
|
||||||
|
|
||||||
.havetracks {
|
|
||||||
padding: 0 5px; /* Add some padding around the text */
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="javascriptIncludes()">
|
<%def name="javascriptIncludes()">
|
||||||
${parent.javascriptIncludes()} <%-- Ensure parent javascript includes are kept --%>
|
<script src="js/libs/jquery.unveil.min.js"></script>
|
||||||
<script src="js/libs/jquery.unveil.min.js"></script> <%-- Keep if still using for lazy loading --%>
|
|
||||||
<script src="js/libs/jquery.dataTables.min.js"></script>
|
<script src="js/libs/jquery.dataTables.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Encapsulate page-specific logic
|
function initThisPage() {
|
||||||
var IndexPage = IndexPage || {};
|
|
||||||
|
|
||||||
IndexPage.initDataTable = function() {
|
|
||||||
$('#artist_table').dataTable({
|
$('#artist_table').dataTable({
|
||||||
"bDestroy": true,
|
"bDestroy": true,
|
||||||
"aoColumnDefs": [
|
"aoColumnDefs": [
|
||||||
@@ -74,17 +36,14 @@
|
|||||||
"aTargets": [0],
|
"aTargets": [0],
|
||||||
"mData":"ArtistID",
|
"mData":"ArtistID",
|
||||||
"mRender": function ( data, type, full ) {
|
"mRender": function ( data, type, full ) {
|
||||||
// Using a class for the image, and data-src for lazy loading
|
return '<div id="artistImg"><img class="albumArt" height="50" width="50" alt="" id="'+ data + '" data-src="artwork/thumbs/artist/' + data + '"/></div>';
|
||||||
// Added loading="lazy" for native lazy loading support where available
|
|
||||||
return '<div class="artistImg-container"><img class="albumArt-thumb" alt="Album art for ' + full['ArtistName'] + '" data-src="artwork/thumbs/artist/' + data + '" loading="lazy" /></div>';
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"aTargets":[1],
|
"aTargets":[1],
|
||||||
"mDataProp":"ArtistSortName",
|
"mDataProp":"ArtistSortName",
|
||||||
"mRender":function (data,type,full) {
|
"mRender":function (data,type,full) {
|
||||||
// Using a class for the span, not an ID
|
return '<span title="' + full['ArtistID'] + '"></span><a href="artistPage?ArtistID=' + full['ArtistID'] + '">' + full['ArtistName'] + '</a>'
|
||||||
return '<span class="artist-sort-name" title="' + full['ArtistID'] + '"></span><a href="artistPage?ArtistID=' + full['ArtistID'] + '">' + full['ArtistName'] + '</a>'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -94,52 +53,52 @@
|
|||||||
"aTargets":[3],
|
"aTargets":[3],
|
||||||
"mDataProp":"LatestAlbum",
|
"mDataProp":"LatestAlbum",
|
||||||
"mRender":function(data,type,full){
|
"mRender":function(data,type,full){
|
||||||
var artist = full; // Renamed to avoid confusion with outer scope
|
artist = full;
|
||||||
var releasedate = '';
|
if (artist['ReleaseDate'] && artist['LatestAlbum'])
|
||||||
var albumdisplay = '<i>None</i>';
|
{
|
||||||
var grade = 'gradeZ'; // Default grade
|
|
||||||
|
|
||||||
if (artist['ReleaseDate'] && artist['LatestAlbum']) {
|
|
||||||
releasedate = artist['ReleaseDate'];
|
releasedate = artist['ReleaseDate'];
|
||||||
albumdisplay = '<i>' + artist['LatestAlbum'] + '</i> (' + artist['ReleaseDate'] + ')';
|
albumdisplay = '<i>' + artist['LatestAlbum'] + '</i> (' + artist['ReleaseDate'] + ')';
|
||||||
} else if (artist['LatestAlbum']) {
|
}
|
||||||
|
else if(artist['LatestAlbum'])
|
||||||
|
{
|
||||||
|
releasedate = '';
|
||||||
albumdisplay = '<i>' + artist['LatestAlbum'] + '</i>';
|
albumdisplay = '<i>' + artist['LatestAlbum'] + '</i>';
|
||||||
}
|
}
|
||||||
|
else
|
||||||
if (artist['ReleaseInFuture'] === 'True') {
|
{
|
||||||
|
releasedate = '';
|
||||||
|
albumdisplay = '<i>None</i>';
|
||||||
|
}
|
||||||
|
if (artist['ReleaseInFuture'] === 'True')
|
||||||
|
{
|
||||||
grade = 'gradeA';
|
grade = 'gradeA';
|
||||||
}
|
}
|
||||||
// artist['Grade'] is used in fnRowCallback, ensure it's set in the data.
|
else
|
||||||
// If this 'Grade' is only for client-side sorting/filtering, it's fine.
|
{
|
||||||
// If it affects server-side logic, it should be handled there.
|
grade = 'gradeZ';
|
||||||
full['Grade'] = grade; // Ensure grade is part of the row data for fnRowCallback
|
}
|
||||||
|
artist['Grade'] = grade;
|
||||||
// Using a class for the span, not an ID
|
return '<span title="' + releasedate + '"></span><a href="albumPage?AlbumID=' + full['AlbumID'] + '">' + albumdisplay + '</a>'
|
||||||
return '<span class="release-date-sort" title="' + releasedate + '"></span><a href="albumPage?AlbumID=' + full['AlbumID'] + '">' + albumdisplay + '</a>'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"aTargets":[4],
|
"aTargets":[4],
|
||||||
"mDataProp":"HaveTracks",
|
"mDataProp":"HaveTracks",
|
||||||
"mRender":function(data,type,full){
|
"mRender":function(data,type,full){
|
||||||
var percent = 0;
|
if(full['TotalTracks'] > 0)
|
||||||
var totalTracksDisplay = '?';
|
{
|
||||||
|
percent = (full['HaveTracks']*100.0)/full['TotalTracks']
|
||||||
if (full['TotalTracks'] > 0) {
|
|
||||||
percent = (full['HaveTracks']*100.0)/full['TotalTracks'];
|
|
||||||
if(percent > 100){
|
if(percent > 100){
|
||||||
percent = 100;
|
percent = 100;
|
||||||
}
|
}
|
||||||
totalTracksDisplay = full['TotalTracks'];
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
full['TotalTracks'] = '?';
|
||||||
|
percent = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Added ARIA attributes for accessibility
|
return '<span title="' + percent + '"></span><div class="progress-container"><div style="width:' + percent + '%"><div class="havetracks">' + full['HaveTracks'] + '/' + full['TotalTracks'] + '</div></div></div>';
|
||||||
return '<span class="have-tracks-sort" title="' + percent + '"></span>' +
|
|
||||||
'<div class="progress-container" role="progressbar" aria-valuenow="' + Math.round(percent) + '" aria-valuemin="0" aria-valuemax="100">' +
|
|
||||||
'<div style="width:' + percent + '%">' +
|
|
||||||
'<div class="havetracks">' + full['HaveTracks'] + '/' + totalTracksDisplay + '</div>' +
|
|
||||||
'</div>' +
|
|
||||||
'</div>';
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -151,73 +110,46 @@
|
|||||||
"sInfoFiltered":"(filtered from _MAX_ total artists)",
|
"sInfoFiltered":"(filtered from _MAX_ total artists)",
|
||||||
"sEmptyTable": " ",
|
"sEmptyTable": " ",
|
||||||
},
|
},
|
||||||
"bStateSave": true, // Retain table state across page loads
|
"bStateSave": true,
|
||||||
"iDisplayLength": 50,
|
"iDisplayLength": 50,
|
||||||
"sPaginationType": "full_numbers",
|
"sPaginationType": "full_numbers",
|
||||||
"bProcessing": true, // Show processing indicator
|
"bProcessing": true,
|
||||||
"bServerSide": true, // Enable server-side processing
|
"bServerSide": true,
|
||||||
"sAjaxSource": 'getArtists.json', // API endpoint for data
|
"sAjaxSource": 'getArtists.json',
|
||||||
"fnRowCallback": function(nRow, aData, iDisplayIndex, iDisplayIndexFull) {
|
"fnRowCallback": function(nRow, aData, iDisplayIndex, iDisplayIndexFull) {
|
||||||
// Apply the 'Grade' class from aData to the table row
|
$('td', nRow).closest('tr').addClass(aData['Grade'])
|
||||||
$(nRow).addClass(aData['Grade']);
|
nRow.children[0].id = 'albumart';
|
||||||
|
nRow.children[1].id = 'name';
|
||||||
// Removed setting duplicate IDs on child elements. Use classes if needed for styling.
|
nRow.children[2].id = 'status'
|
||||||
// For example:
|
nRow.children[3].id = 'album'
|
||||||
// $(nRow).find('td:eq(0)').addClass('albumart-cell');
|
nRow.children[4].id = 'have'
|
||||||
// $(nRow).find('td:eq(1)').addClass('name-cell');
|
|
||||||
// etc.
|
|
||||||
return nRow;
|
return nRow;
|
||||||
},
|
},
|
||||||
"fnServerData": function ( sSource, aoData, fnCallback ) {
|
"fnServerData": function ( sSource, aoData, fnCallback ) {
|
||||||
// Custom function for fetching data, using $.getJSON
|
/* Add some extra data to the sender */
|
||||||
$.getJSON( sSource, aoData, function (json) {
|
$.getJSON( sSource, aoData, function (json) { fnCallback(json) } )
|
||||||
fnCallback(json);
|
},
|
||||||
}).fail(function(jqXHR, textStatus, errorThrown) {
|
"fnInitComplete": function(oSettings, json)
|
||||||
console.error("Error fetching artist data:", textStatus, errorThrown);
|
{
|
||||||
// Provide user feedback if data loading fails
|
|
||||||
// e.g., show an error message in the table or a global notification
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
// Removed fnInitComplete as it was empty
|
|
||||||
"fnDrawCallback": function (o) {
|
"fnDrawCallback": function (o) {
|
||||||
// Jump to top of page
|
// Jump to top of page
|
||||||
$('html,body').scrollTop(0);
|
$('html,body').scrollTop(0);
|
||||||
// Re-unveil images after each draw for lazy loading
|
|
||||||
$("img.albumArt-thumb").unveil();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// jQuery Unveil listener for new images added to the DOM after DataTables draw
|
$('#artist_table').on("draw.dt", function () {
|
||||||
// This is handled in fnDrawCallback now for better integration with DataTables.
|
$("img").unveil();
|
||||||
// $('#artist_table').on("draw.dt", function () {
|
});
|
||||||
// $("img.albumArt-thumb").unveil();
|
|
||||||
// });
|
|
||||||
|
|
||||||
// Assuming resetFilters is a global function from common.js
|
|
||||||
if (typeof resetFilters === 'function') {
|
|
||||||
resetFilters("artist or album");
|
resetFilters("artist or album");
|
||||||
} else {
|
|
||||||
console.warn("resetFilters function is not defined.");
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
IndexPage.initDataTable();
|
initThisPage();
|
||||||
});
|
});
|
||||||
|
$(window).load(function(){
|
||||||
$(window).on('load', function(){
|
|
||||||
// Ensure these functions exist and are necessary.
|
|
||||||
// If initFancybox or refreshLoadArtist are global, call them here.
|
|
||||||
// Otherwise, they should be encapsulated or refactored.
|
|
||||||
if (typeof initFancybox === 'function') {
|
|
||||||
initFancybox();
|
initFancybox();
|
||||||
} else {
|
|
||||||
console.warn("initFancybox function is not defined.");
|
|
||||||
}
|
|
||||||
if (typeof refreshLoadArtist === 'function') {
|
|
||||||
refreshLoadArtist();
|
refreshLoadArtist();
|
||||||
} else {
|
|
||||||
console.warn("refreshLoadArtist function is not defined.");
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|||||||
@@ -1,539 +1,386 @@
|
|||||||
/**
|
function getThumb(imgElem,id,type) {
|
||||||
* @file headphones.js
|
|
||||||
* @brief Main JavaScript file for Headphones web interface.
|
|
||||||
* Contains core UI, API, and utility functions.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var Headphones = Headphones || {};
|
if ( type == 'artist' ) {
|
||||||
|
var thumbURL = "getThumb?ArtistID=" + id;
|
||||||
// --- Configuration ---
|
// var imgURL = "getArtwork?ArtistID=" + id;
|
||||||
Headphones.config = {
|
|
||||||
fallbackImage: "interfaces/default/images/no-cover-art.png", // Generic fallback
|
|
||||||
fallbackArtistImage: "interfaces/default/images/no-cover-artist.png", // Specific artist fallback
|
|
||||||
messageTimeout: 3000 // Default timeout for messages in milliseconds
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- UI Messaging Module ---
|
|
||||||
Headphones.UI = Headphones.UI || {};
|
|
||||||
Headphones.UI.Message = (function() {
|
|
||||||
var $ajaxMsg = $("#ajaxMsg");
|
|
||||||
var $ajaxMsg2 = $("#ajaxMsg2"); // Assuming this is for artist-specific messages
|
|
||||||
var $updateBar = $("#updatebar");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows a feedback message to the user.
|
|
||||||
* @param {string} msg - The message to display.
|
|
||||||
* @param {string} type - Type of message ('success', 'error', 'info', 'loading').
|
|
||||||
* @param {number} [timeout=Headphones.config.messageTimeout] - Duration to display the message (in ms). Set to 0 for no timeout.
|
|
||||||
* @param {HTMLElement} [$targetElement=$ajaxMsg] - The jQuery element to display the message in.
|
|
||||||
*/
|
|
||||||
function showMessage(msg, type, timeout, $targetElement) {
|
|
||||||
timeout = typeof timeout !== 'undefined' ? timeout : Headphones.config.messageTimeout;
|
|
||||||
$targetElement = $targetElement || $ajaxMsg;
|
|
||||||
|
|
||||||
// Adjust position if update bar is visible
|
|
||||||
if ($updateBar.is(":visible")) {
|
|
||||||
var height = $updateBar.height() + 35;
|
|
||||||
$targetElement.css("bottom", height + "px");
|
|
||||||
} else {
|
} else {
|
||||||
$targetElement.removeAttr("style");
|
var thumbURL = "getThumb?AlbumID=" + id;
|
||||||
|
// var imgURL = "getArtwork?AlbumID=" + id;
|
||||||
}
|
}
|
||||||
|
// Get Data from the cache by Artist ID
|
||||||
$targetElement.fadeIn();
|
|
||||||
$targetElement.removeClass('success error info'); // Clear previous states
|
|
||||||
|
|
||||||
var iconClass = '';
|
|
||||||
switch (type) {
|
|
||||||
case 'success':
|
|
||||||
iconClass = 'fa-check';
|
|
||||||
$targetElement.addClass('success');
|
|
||||||
break;
|
|
||||||
case 'error':
|
|
||||||
iconClass = 'fa-exclamation-triangle';
|
|
||||||
$targetElement.addClass('error');
|
|
||||||
break;
|
|
||||||
case 'loading':
|
|
||||||
iconClass = 'fa-refresh fa-spin';
|
|
||||||
break;
|
|
||||||
default: // info
|
|
||||||
iconClass = 'fa-info-circle';
|
|
||||||
$targetElement.addClass('info');
|
|
||||||
}
|
|
||||||
|
|
||||||
var $message = $("<div class='msg'><i class='fa " + iconClass + "'></i> " + msg + "</div>");
|
|
||||||
$targetElement.empty().append($message); // Clear and append new message
|
|
||||||
|
|
||||||
if (timeout > 0) {
|
|
||||||
setTimeout(function() {
|
|
||||||
$message.fadeOut(function() {
|
|
||||||
$(this).remove();
|
|
||||||
$targetElement.fadeOut();
|
|
||||||
});
|
|
||||||
}, timeout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specific function for artist messages (if still needed, otherwise consolidate into showMessage)
|
|
||||||
* @param {string} msg - The message to display.
|
|
||||||
*/
|
|
||||||
function showArtistMessage(msg) {
|
|
||||||
showMessage(msg, 'loading', 0, $ajaxMsg2); // No timeout for loading message
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
show: showMessage,
|
|
||||||
showArtist: showArtistMessage // Consider removing if not distinct enough
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
// --- API / AJAX Module ---
|
|
||||||
Headphones.API = (function() {
|
|
||||||
/**
|
|
||||||
* Performs an AJAX call with standardized messaging and reload options.
|
|
||||||
* @param {object} options - Configuration options for the AJAX call.
|
|
||||||
* @param {string} options.url - The URL for the AJAX request.
|
|
||||||
* @param {object|string} [options.data] - Data to send with the request.
|
|
||||||
* @param {string} [options.type='POST'] - HTTP method ('GET' or 'POST').
|
|
||||||
* @param {string} [options.successMessage='Success!'] - Message to display on success.
|
|
||||||
* @param {string} [options.errorMessage='There was an error'] - Message to display on error.
|
|
||||||
* @param {string} [options.reloadType] - Type of reload after success ('table', 'tabs', 'page', 'submenu', 'submenu&table').
|
|
||||||
* @param {HTMLElement} [options.contextElement] - The DOM element that triggered the action (for data-attributes).
|
|
||||||
* @param {function} [options.beforeSendCallback] - Custom function to call before sending.
|
|
||||||
* @param {function} [options.successCallback] - Custom function to call on success.
|
|
||||||
* @param {function} [options.errorCallback] - Custom function to call on error.
|
|
||||||
* @param {function} [options.completeCallback] - Custom function to call on completion.
|
|
||||||
*/
|
|
||||||
function doAjaxCall(options) {
|
|
||||||
var opts = $.extend(true, {
|
|
||||||
url: '',
|
|
||||||
data: {},
|
|
||||||
type: 'POST',
|
|
||||||
successMessage: 'Success!',
|
|
||||||
errorMessage: 'There was an error',
|
|
||||||
reloadType: null, // 'table', 'tabs', 'page', 'submenu', 'submenu&table'
|
|
||||||
contextElement: null,
|
|
||||||
beforeSendCallback: null,
|
|
||||||
successCallback: null,
|
|
||||||
errorCallback: null,
|
|
||||||
completeCallback: null
|
|
||||||
}, options);
|
|
||||||
|
|
||||||
// Get messages from data attributes if context element provided
|
|
||||||
if (opts.contextElement) {
|
|
||||||
var $elem = $(opts.contextElement);
|
|
||||||
opts.successMessage = $elem.data('success') || opts.successMessage;
|
|
||||||
opts.errorMessage = $elem.data('error') || opts.errorMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: opts.url,
|
url: thumbURL,
|
||||||
data: opts.data,
|
cache: true,
|
||||||
type: opts.type,
|
success: function(data){
|
||||||
beforeSend: function(jqXHR, settings) {
|
if ( data == "" ) {
|
||||||
Headphones.UI.Message.show('Loading...', 'loading', 0); // Show loading message indefinitely
|
var imageUrl = "interfaces/default/images/no-cover-artist.png";
|
||||||
if (opts.beforeSendCallback) opts.beforeSendCallback(jqXHR, settings);
|
|
||||||
},
|
|
||||||
error: function(jqXHR, textStatus, errorThrown) {
|
|
||||||
Headphones.UI.Message.show(opts.errorMessage, 'error');
|
|
||||||
if (opts.errorCallback) opts.errorCallback(jqXHR, textStatus, errorThrown);
|
|
||||||
},
|
|
||||||
success: function(data, textStatus, jqXHR) {
|
|
||||||
Headphones.UI.Message.show(opts.successMessage, 'success');
|
|
||||||
if (opts.successCallback) opts.successCallback(data, textStatus, jqXHR);
|
|
||||||
|
|
||||||
// Handle reloads based on reloadType
|
|
||||||
setTimeout(function() { // Wait for message to fade out
|
|
||||||
if (opts.reloadType === 'table') {
|
|
||||||
Headphones.UI.Content.refreshTable();
|
|
||||||
} else if (opts.reloadType === 'tabs') {
|
|
||||||
Headphones.UI.Content.refreshTab();
|
|
||||||
} else if (opts.reloadType === 'page') {
|
|
||||||
location.reload();
|
|
||||||
} else if (opts.reloadType === 'submenu') {
|
|
||||||
Headphones.UI.Content.refreshSubmenu();
|
|
||||||
} else if (opts.reloadType === 'submenu&table') {
|
|
||||||
Headphones.UI.Content.refreshSubmenu();
|
|
||||||
Headphones.UI.Content.refreshTable();
|
|
||||||
}
|
}
|
||||||
}, Headphones.config.messageTimeout + 100); // Wait for message fade out
|
else {
|
||||||
},
|
var imageUrl = data;
|
||||||
complete: function(jqXHR, textStatus) {
|
}
|
||||||
// The message will fade out automatically by showMessage
|
$(imgElem).attr("src",imageUrl).hide().fadeIn();
|
||||||
if (opts.completeCallback) opts.completeCallback(jqXHR, textStatus);
|
// $(imgElem).wrap('<a href="'+ imgURL +'" rel="dialog" title="' + name + '"></a>');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function getArtwork(imgElem,id,name,type) {
|
||||||
* Performs a simple GET AJAX call without complex messaging or reload.
|
|
||||||
* @param {string} url - The URL for the GET request.
|
if ( type == 'artist' ) {
|
||||||
*/
|
var artworkURL = "getArtwork?ArtistID=" + id;
|
||||||
function doSimpleAjaxCall(url) {
|
} else {
|
||||||
$.ajax({ url: url, type: 'GET' });
|
var artworkURL = "getArtwork?AlbumID=" + id;
|
||||||
|
}
|
||||||
|
// Get Data from the cache by Artist ID
|
||||||
|
$.ajax({
|
||||||
|
url: artworkURL,
|
||||||
|
cache: true,
|
||||||
|
success: function(data){
|
||||||
|
if ( data == "" || data == undefined ) {
|
||||||
|
var imageUrl = "interfaces/default/images/no-cover-artist.png";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var imageUrl = data;
|
||||||
|
}
|
||||||
|
$(imgElem).attr("src",imageUrl).hide().fadeIn();
|
||||||
|
$(imgElem).wrap('<a href="'+ imageUrl +'" rel="dialog" title="' + name + '"></a>');
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
function getInfo(elem,id,type) {
|
||||||
call: doAjaxCall,
|
|
||||||
simpleCall: doSimpleAjaxCall
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
// --- Image Loading Module ---
|
|
||||||
Headphones.Images = (function() {
|
|
||||||
/**
|
|
||||||
* Loads artwork/thumbnail for an element, handling lazy load and fallbacks.
|
|
||||||
* @param {jQuery} $imgElem - The jQuery image element.
|
|
||||||
* @param {string} id - The ID (AlbumID or ArtistID).
|
|
||||||
* @param {string} type - 'album' or 'artist'.
|
|
||||||
* @param {boolean} [unveil=false] - Whether to use data-src and unveil for lazy loading.
|
|
||||||
*/
|
|
||||||
function loadImage($imgElem, id, type, unveil) {
|
|
||||||
var infoURL = "getImageLinks?" + (type === 'artist' ? "ArtistID=" : "AlbumID=") + id;
|
|
||||||
|
|
||||||
|
if ( type == 'artist' ) {
|
||||||
|
var infoURL = "getInfo?ArtistID=" + id;
|
||||||
|
} else {
|
||||||
|
var infoURL = "getInfo?AlbumID=" + id;
|
||||||
|
}
|
||||||
|
// Get Data from the cache by ID
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: infoURL,
|
url: infoURL,
|
||||||
cache: true,
|
cache: true,
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
success: function(data){
|
success: function(data){
|
||||||
var imageUrl = Headphones.config.fallbackImage;
|
var summary = data.Summary;
|
||||||
var artworkUrl = Headphones.config.fallbackImage;
|
$(elem).append(summary);
|
||||||
|
|
||||||
if (data) {
|
|
||||||
imageUrl = data.thumbnail || (type === 'artist' ? Headphones.config.fallbackArtistImage : Headphones.config.fallbackImage);
|
|
||||||
artworkUrl = data.artwork || (type === 'artist' ? Headphones.config.fallbackArtistImage : Headphones.config.fallbackImage);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (unveil) {
|
|
||||||
$imgElem.attr("data-src", imageUrl);
|
|
||||||
$imgElem.unveil(); // Trigger unveil for lazy loading
|
|
||||||
} else {
|
|
||||||
$imgElem.attr("src", imageUrl).hide().fadeIn(); // Direct load with fade
|
|
||||||
}
|
|
||||||
|
|
||||||
// If element has a rel="dialog" parent, update href for Fancybox
|
|
||||||
var $wrapper = $imgElem.closest('a[rel="dialog"]');
|
|
||||||
if ($wrapper.length) {
|
|
||||||
$wrapper.attr('href', artworkUrl); // Link to full artwork
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: function() {
|
|
||||||
// On error, set to fallback image if not already set or handled by onerror on HTML
|
|
||||||
if (unveil) {
|
|
||||||
$imgElem.attr("data-src", Headphones.config.fallbackImage);
|
|
||||||
$imgElem.unveil();
|
|
||||||
} else {
|
|
||||||
$imgElem.attr("src", Headphones.config.fallbackImage).hide().fadeIn();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function getImageLinks(elem,id,type,unveil) {
|
||||||
* Fetches and appends summary information.
|
if ( type == 'artist' ) {
|
||||||
* @param {jQuery} $elem - The element to append summary to.
|
var infoURL = "getImageLinks?ArtistID=" + id;
|
||||||
* @param {string} id - The ID (AlbumID or ArtistID).
|
} else {
|
||||||
* @param {string} type - 'album' or 'artist'.
|
var infoURL = "getImageLinks?AlbumID=" + id;
|
||||||
*/
|
}
|
||||||
function getInfo($elem, id, type) {
|
|
||||||
var infoURL = "getInfo?" + (type === 'artist' ? "ArtistID=" : "AlbumID=") + id;
|
// Get Data from the cache by ID
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: infoURL,
|
url: infoURL,
|
||||||
cache: true,
|
cache: true,
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
success: function(data){
|
success: function(data){
|
||||||
if (data && data.Summary) {
|
if (!data) {
|
||||||
$elem.append(data.Summary);
|
// Invalid response
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.thumbnail) {
|
||||||
|
var thumbnail = "interfaces/default/images/no-cover-artist.png";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var thumbnail = data.thumbnail;
|
||||||
|
}
|
||||||
|
if (!data.artwork) {
|
||||||
|
var artwork = "interfaces/default/images/no-cover-artist.png";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var artwork = data.artwork;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unveil) {
|
||||||
|
$(elem).attr("data-src", thumbnail);
|
||||||
|
$(elem).unveil();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$(elem).attr("src", thumbnail);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
load: loadImage,
|
|
||||||
getInfo: getInfo
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
// --- UI Content Refresh Module ---
|
|
||||||
Headphones.UI.Content = (function() {
|
|
||||||
/**
|
|
||||||
* Refreshes a specific part of the page content.
|
|
||||||
* @param {string} targetSelector - jQuery selector for the element to load content into.
|
|
||||||
* @param {string} sourceContentSelector - jQuery selector for the content to extract from the source URL.
|
|
||||||
* @param {function} [initCallback] - Callback function to run after content is loaded.
|
|
||||||
*/
|
|
||||||
function refreshPart(targetSelector, sourceContentSelector, initCallback) {
|
|
||||||
var url = window.location.href;
|
|
||||||
$(targetSelector).load(url + " " + sourceContentSelector, function(response, status, xhr) {
|
|
||||||
if (status === "error") {
|
|
||||||
console.error("Failed to load content for " + targetSelector + ": " + xhr.status + " " + xhr.statusText);
|
|
||||||
Headphones.UI.Message.show("Failed to refresh content.", 'error');
|
|
||||||
} else if (initCallback) {
|
|
||||||
// Ensure the callback is part of the Headphones object if it relies on it
|
|
||||||
if (typeof initCallback === 'string' && Headphones.hasOwnProperty(initCallback)) {
|
|
||||||
Headphones[initCallback](); // Call the global Headphones function
|
|
||||||
} else if (typeof initCallback === 'function') {
|
|
||||||
initCallback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Refreshes the submenu by reloading its content. */
|
|
||||||
function refreshSubmenu() {
|
|
||||||
refreshPart("#subhead_container", "#subhead_menu", Headphones.UI.initActions); // Pass initActions to re-apply handlers
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Refreshes the main table content. */
|
|
||||||
function refreshTable() {
|
|
||||||
// This assumes there's one main .display table. If multiple, refine selector.
|
|
||||||
// The original also reloaded tbody and thead separately, which is fine.
|
|
||||||
refreshPart("table.display", "table.display tbody, table.display thead", function() {
|
|
||||||
// Re-initialize DataTables after table refresh.
|
|
||||||
// This might need specific DataTable re-initialization logic for each page.
|
|
||||||
// A more robust solution would be to destroy and recreate the DataTable.
|
|
||||||
// The template-specific `initThisPage()` usually contains this.
|
|
||||||
// We need to call the appropriate initThisPage for the current page.
|
|
||||||
// As `initThisPage` is defined per HTML file, we rely on the specific page's ready handler
|
|
||||||
// to call its own initThisPage, or pass it as a parameter if this is called generically.
|
|
||||||
// For now, let's assume `initThisPage` exists in the global scope where refreshTable is called.
|
|
||||||
if (typeof initThisPage === 'function') {
|
|
||||||
initThisPage();
|
|
||||||
} else if (typeof SearchResultsPage !== 'undefined' && typeof SearchResultsPage.initDataTable === 'function') {
|
|
||||||
SearchResultsPage.initDataTable();
|
|
||||||
}
|
|
||||||
// ... add more page-specific re-initializations as needed
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Refreshes the currently active tab content. */
|
|
||||||
function refreshTab() {
|
|
||||||
var tabId = $('.ui-tabs-panel:visible').attr("id");
|
|
||||||
if (tabId) {
|
|
||||||
refreshPart('.ui-tabs-panel:visible', "#" + tabId, function() {
|
|
||||||
// Similar to refreshTable, re-initialize specific tab content
|
|
||||||
if (typeof initThisPage === 'function') { // assuming initThisPage handles tab content
|
|
||||||
initThisPage();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Polls for status changes on rows with 'gradeL' class (loading state).
|
|
||||||
* This is an older polling pattern; consider WebSockets for real-time updates if performance is critical.
|
|
||||||
*/
|
|
||||||
function refreshLoadArtist() {
|
|
||||||
var $loadingRows = $("table.display tr.gradeL");
|
|
||||||
if ($loadingRows.length > 0) {
|
|
||||||
$loadingRows.each(function() {
|
|
||||||
var $row = $(this);
|
|
||||||
// Reload only the row's content to check status
|
|
||||||
var rowIndex = $row.index() + 1; // 1-based index
|
|
||||||
var url = window.location.href;
|
|
||||||
$row.load(url + " table.display tbody tr:nth-child(" + rowIndex + ") td", function(response, status, xhr) {
|
|
||||||
if (status === "error") {
|
|
||||||
console.error("Failed to load row status: " + xhr.status + " " + xhr.statusText);
|
|
||||||
// Optional: Show a message to the user
|
|
||||||
} else {
|
|
||||||
// Check status after loading the updated row content
|
|
||||||
if ($row.find(".column-status").text() === 'Active') { // Assuming a status column with this text
|
|
||||||
$row.removeClass('gradeL').addClass('gradeZ');
|
|
||||||
// Re-initialize relevant JS if needed for the row (e.g., DataTables redrawing)
|
|
||||||
if (typeof initThisPage === 'function') initThisPage(); // Re-initialize the table/page
|
|
||||||
} else {
|
|
||||||
// If still loading, set timeout for next check
|
|
||||||
setTimeout(refreshLoadArtist, 3000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return {
|
|
||||||
refreshSubmenu: refreshSubmenu,
|
|
||||||
refreshTable: refreshTable,
|
|
||||||
refreshTab: refreshTab,
|
|
||||||
refreshLoadArtist: refreshLoadArtist
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
// --- UI Element Initialization Module ---
|
|
||||||
Headphones.UI.Elements = (function() {
|
|
||||||
/** Initializes the header scroll-fade effect. */
|
|
||||||
function initHeader() {
|
function initHeader() {
|
||||||
var $header = $("#container header");
|
//settings
|
||||||
var fadeSpeed = 100,
|
var header = $("#container header");
|
||||||
fadeTo = 0.5,
|
var fadeSpeed = 100, fadeTo = 0.5, topDistance = 20;
|
||||||
topDistance = 20;
|
var topbarME = function() { $(header).fadeTo(fadeSpeed,1); }, topbarML = function() { $(header).fadeTo(fadeSpeed,fadeTo); };
|
||||||
var inside = false;
|
var inside = false;
|
||||||
|
//do
|
||||||
$(window).on('scroll', function() {
|
$(window).scroll(function() {
|
||||||
var position = $(window).scrollTop();
|
position = $(window).scrollTop();
|
||||||
if(position > topDistance && !inside) {
|
if(position > topDistance && !inside) {
|
||||||
//add events
|
//add events
|
||||||
$header.fadeTo(fadeSpeed, fadeTo);
|
topbarML();
|
||||||
$header.on('mouseenter', function() {
|
$(header).bind('mouseenter',topbarME);
|
||||||
$header.fadeTo(fadeSpeed, 1);
|
$(header).bind('mouseleave',topbarML);
|
||||||
});
|
|
||||||
$header.on('mouseleave', function() {
|
|
||||||
$header.fadeTo(fadeSpeed, fadeTo);
|
|
||||||
});
|
|
||||||
$("#toTop").fadeIn();
|
$("#toTop").fadeIn();
|
||||||
inside = true;
|
inside = true;
|
||||||
} else if (position < topDistance && inside) { // Added '&& inside' to prevent re-triggering
|
}
|
||||||
$header.fadeTo(fadeSpeed, 1); // Ensure it's fully visible at top
|
else if (position < topDistance){
|
||||||
$header.off('mouseenter mouseleave'); // Remove events when at top
|
topbarME();
|
||||||
|
$(header).unbind('mouseenter',topbarME);
|
||||||
|
$(header).unbind('mouseleave',topbarML);
|
||||||
$("#toTop").fadeOut();
|
$("#toTop").fadeOut();
|
||||||
inside = false;
|
inside = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes config checkboxes to show/hide sibling content.
|
|
||||||
* @param {HTMLElement} elem - The checkbox DOM element.
|
|
||||||
*/
|
|
||||||
function initConfigCheckbox(elem) {
|
function initConfigCheckbox(elem) {
|
||||||
var $checkbox = $(elem);
|
var config = $(elem).parent().next();
|
||||||
var $configContent = $checkbox.parent().next();
|
if ( $(elem).is(":checked") ) {
|
||||||
|
config.show();
|
||||||
// Initial state
|
|
||||||
if ($checkbox.is(":checked")) {
|
|
||||||
$configContent.show();
|
|
||||||
} else {
|
} else {
|
||||||
$configContent.hide();
|
config.hide();
|
||||||
|
}
|
||||||
|
$(elem).click(function(){
|
||||||
|
var config = $(this).parent().next();
|
||||||
|
if ( $(this).is(":checked") ) {
|
||||||
|
config.slideDown();
|
||||||
|
} else {
|
||||||
|
config.slideUp();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function initActions() {
|
||||||
|
$("#subhead_menu #menu_link_refresh").button();
|
||||||
|
$("#subhead_menu #menu_link_edit").button();
|
||||||
|
$("#subhead_menu .menu_link_edit").button();
|
||||||
|
$("#subhead_menu #menu_link_delete" ).button();
|
||||||
|
$("#subhead_menu #menu_link_pauze").button();
|
||||||
|
$("#subhead_menu #menu_link_resume").button();
|
||||||
|
$("#subhead_menu #menu_link_getextra").button();
|
||||||
|
$("#subhead_menu #menu_link_removeextra").button();
|
||||||
|
$("#subhead_menu #menu_link_wanted" ).button();
|
||||||
|
$("#subhead_menu #menu_link_check").button();
|
||||||
|
$("#subhead_menu #menu_link_skipped").button();
|
||||||
|
$("#subhead_menu #menu_link_retry").button();
|
||||||
|
$("#subhead_menu #menu_link_new").button();
|
||||||
|
$("#subhead_menu #menu_link_shutdown").button();
|
||||||
|
$("#subhead_menu #menu_link_scan").button();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Click handler
|
function refreshSubmenu() {
|
||||||
$checkbox.on('click', function() {
|
var url = $(location).attr('href');
|
||||||
if ($(this).is(":checked")) {
|
$("#subhead_container").load(url + " #subhead_menu",function(){
|
||||||
$configContent.slideDown();
|
initActions();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function refreshTable() {
|
||||||
|
var url = $(location).attr('href');
|
||||||
|
$("table.display").load(url + " table.display tbody, table.display thead", function() {
|
||||||
|
initThisPage();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function refreshLoadArtist() {
|
||||||
|
if ( $(".gradeL").length > 0 ) {
|
||||||
|
var url = $(location).attr('href');
|
||||||
|
var loadingRow = $("table.display tr.gradeL")
|
||||||
|
loadingRow.each(function(){
|
||||||
|
var row = $(this).index() + 1;
|
||||||
|
var rowLoad = $("table.display tbody tr:nth-child("+row+")");
|
||||||
|
$(rowLoad).load(url + " table.display tbody tr:nth-child("+ row +") td", function() {
|
||||||
|
if ( $(rowLoad).children("#status").text() == 'Active' ) {
|
||||||
|
// Active
|
||||||
|
$(rowLoad).removeClass('gradeL').addClass('gradeZ');
|
||||||
|
initThisPage();
|
||||||
} else {
|
} else {
|
||||||
$configContent.slideUp();
|
// Still loading
|
||||||
|
setTimeout(function(){
|
||||||
|
refreshLoadArtist();
|
||||||
|
},3000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshTab() {
|
||||||
|
var url = $(location).attr('href');
|
||||||
|
var tabId = $('.ui-tabs-panel:visible').attr("id");
|
||||||
|
$('.ui-tabs-panel:visible').load(url + " #"+ tabId, function() {
|
||||||
|
initThisPage();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showMsg(msg,loader,timeout,ms) {
|
||||||
|
var feedback = $("#ajaxMsg");
|
||||||
|
update = $("#updatebar");
|
||||||
|
if ( update.is(":visible") ) {
|
||||||
|
var height = update.height() + 35;
|
||||||
|
feedback.css("bottom",height + "px");
|
||||||
|
} else {
|
||||||
|
feedback.removeAttr("style");
|
||||||
|
}
|
||||||
|
feedback.fadeIn();
|
||||||
|
var message = $("<div class='msg'>" + msg + "</div>");
|
||||||
|
if (loader) {
|
||||||
|
var message = $("<i class='fa fa-refresh fa-spin'></i> " + msg + "</div>");
|
||||||
|
feedback.css("padding","14px 10px")
|
||||||
|
}
|
||||||
|
$(feedback).prepend(message);
|
||||||
|
if (timeout) {
|
||||||
|
setTimeout(function(){
|
||||||
|
message.fadeOut(function(){
|
||||||
|
$(this).remove();
|
||||||
|
feedback.fadeOut();
|
||||||
|
});
|
||||||
|
},ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showArtistMsg(msg) {
|
||||||
|
var feedback = $("#ajaxMsg2");
|
||||||
|
update = $("#updatebar");
|
||||||
|
if ( update.is(":visible") ) {
|
||||||
|
var height = update.height() + 35;
|
||||||
|
feedback.css("bottom",height + "px");
|
||||||
|
} else {
|
||||||
|
feedback.removeAttr("style");
|
||||||
|
}
|
||||||
|
feedback.fadeIn();
|
||||||
|
var message = $("<i class='fa fa-refresh fa-spin'></i> " + msg + "</div>");
|
||||||
|
feedback.css("padding","14px 10px")
|
||||||
|
$(feedback).prepend(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function doAjaxCall(url,elem,reload,form) {
|
||||||
|
// Set Message
|
||||||
|
feedback = $("#ajaxMsg");
|
||||||
|
update = $("#updatebar");
|
||||||
|
if ( update.is(":visible") ) {
|
||||||
|
var height = update.height() + 35;
|
||||||
|
feedback.css("bottom",height + "px");
|
||||||
|
} else {
|
||||||
|
feedback.removeAttr("style");
|
||||||
|
}
|
||||||
|
|
||||||
|
feedback.fadeIn();
|
||||||
|
// Get Form data
|
||||||
|
var formID = "#"+url;
|
||||||
|
if ( form == true ) {
|
||||||
|
var dataString = $(formID).serialize();
|
||||||
|
}
|
||||||
|
// Loader Image
|
||||||
|
var loader = $("<i class='fa fa-refresh fa-spin'></i>");
|
||||||
|
// Data Success Message
|
||||||
|
var dataSucces = $(elem).data('success');
|
||||||
|
if (typeof dataSucces === "undefined") {
|
||||||
|
// Standard Message when variable is not set
|
||||||
|
var dataSucces = "Success!";
|
||||||
|
}
|
||||||
|
// Data Errror Message
|
||||||
|
var dataError = $(elem).data('error');
|
||||||
|
if (typeof dataError === "undefined") {
|
||||||
|
// Standard Message when variable is not set
|
||||||
|
var dataError = "There was an error";
|
||||||
|
}
|
||||||
|
// Get Success & Error message from inline data, else use standard message
|
||||||
|
var succesMsg = $("<div class='msg'><i class='fa fa-check'></i> " + dataSucces + "</div>");
|
||||||
|
var errorMsg = $("<div class='msg'><i class='fa fa-exclamation-triangle'></i> " + dataError + "</div>");
|
||||||
|
|
||||||
|
// Check if checkbox is selected
|
||||||
|
if ( form ) {
|
||||||
|
if ( $('td#select input[type=checkbox]').length > 0 && !$('td#select input[type=checkbox]').is(':checked') || $('#importLastFM #username:visible').length > 0 && $("#importLastFM #username" ).val().length === 0 ) {
|
||||||
|
feedback.addClass('error')
|
||||||
|
$(feedback).prepend(errorMsg);
|
||||||
|
setTimeout(function(){
|
||||||
|
errorMsg.fadeOut(function(){
|
||||||
|
$(this).remove();
|
||||||
|
feedback.fadeOut(function(){
|
||||||
|
feedback.removeClass('error');
|
||||||
|
});
|
||||||
|
})
|
||||||
|
$(formID + " select").children('option[disabled=disabled]').attr('selected','selected');
|
||||||
|
},2000);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajax Call
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
data: dataString,
|
||||||
|
type: 'POST',
|
||||||
|
beforeSend: function(jqXHR, settings) {
|
||||||
|
// Start loader etc.
|
||||||
|
feedback.prepend(loader);
|
||||||
|
},
|
||||||
|
error: function(jqXHR, textStatus, errorThrown) {
|
||||||
|
feedback.addClass('error')
|
||||||
|
feedback.prepend(errorMsg);
|
||||||
|
setTimeout(function(){
|
||||||
|
errorMsg.fadeOut(function(){
|
||||||
|
$(this).remove();
|
||||||
|
feedback.fadeOut(function(){
|
||||||
|
feedback.removeClass('error')
|
||||||
|
});
|
||||||
|
})
|
||||||
|
},2000);
|
||||||
|
},
|
||||||
|
success: function(data,jqXHR) {
|
||||||
|
feedback.prepend(succesMsg);
|
||||||
|
feedback.addClass('success')
|
||||||
|
setTimeout(function(e){
|
||||||
|
succesMsg.fadeOut(function(){
|
||||||
|
$(this).remove();
|
||||||
|
feedback.fadeOut(function(){
|
||||||
|
feedback.removeClass('success');
|
||||||
|
});
|
||||||
|
if ( reload == true ) refreshSubmenu();
|
||||||
|
if ( reload == "table") {
|
||||||
|
console.log('refresh'); refreshTable();
|
||||||
|
}
|
||||||
|
if ( reload == "tabs") refreshTab();
|
||||||
|
if ( reload == "page") location.reload();
|
||||||
|
if ( reload == "submenu&table") {
|
||||||
|
refreshSubmenu();
|
||||||
|
refreshTable();
|
||||||
|
}
|
||||||
|
if ( form ) {
|
||||||
|
// Change the option to 'choose...'
|
||||||
|
$(formID + " select").children('option[disabled=disabled]').attr('selected','selected');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},2000);
|
||||||
|
},
|
||||||
|
complete: function(jqXHR, textStatus) {
|
||||||
|
// Remove loaders and stuff, ajax request is complete!
|
||||||
|
loader.remove();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Initializes various action buttons with jQuery UI button styling. */
|
function doSimpleAjaxCall(url) {
|
||||||
function initActions() {
|
$.ajax(url);
|
||||||
// Select all menu links under #subhead_menu and apply button()
|
|
||||||
// This assumes jQuery UI Button widget is available and desired.
|
|
||||||
$("#subhead_menu a[id^='menu_link_'], #subhead_menu .menu_link_edit").button();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
initHeader: initHeader,
|
|
||||||
initConfigCheckbox: initConfigCheckbox,
|
|
||||||
initActions: initActions
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
// --- Utility Functions Module ---
|
|
||||||
Headphones.Utils = (function() {
|
|
||||||
/**
|
|
||||||
* Sets placeholder text for DataTables search input.
|
|
||||||
* @param {string} text - The placeholder text.
|
|
||||||
*/
|
|
||||||
function resetFilters(text){
|
function resetFilters(text){
|
||||||
// Ensure this targets the correct search input for DataTables
|
if ( $(".dataTables_filter").length > 0 ) {
|
||||||
if ($(".dataTables_filter input").length > 0) {
|
|
||||||
$(".dataTables_filter input").attr("placeholder","filter " + text + "");
|
$(".dataTables_filter input").attr("placeholder","filter " + text + "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Initializes Fancybox for dialog links. */
|
|
||||||
function initFancybox() {
|
function initFancybox() {
|
||||||
// Check if Fancybox script is already loaded or needs to be loaded dynamically
|
if ( $("a[rel=dialog]").length > 0 ) {
|
||||||
if ($.fn.fancybox) { // Check if fancybox function exists on jQuery object
|
|
||||||
$("a[rel=dialog]").fancybox();
|
|
||||||
} else if ($("a[rel=dialog]").length > 0) {
|
|
||||||
// Dynamically load Fancybox script and CSS if elements with rel=dialog exist
|
|
||||||
$.getScript('interfaces/default/js/fancybox/jquery.fancybox-1.3.4.js', function() {
|
$.getScript('interfaces/default/js/fancybox/jquery.fancybox-1.3.4.js', function() {
|
||||||
$("head").append("<link rel='stylesheet' href='interfaces/default/js/fancybox/jquery.fancybox-1.3.4.css'>");
|
$("head").append("<link rel='stylesheet' href='interfaces/default/js/fancybox/jquery.fancybox-1.3.4.css'>");
|
||||||
$("a[rel=dialog]").fancybox();
|
$("a[rel=dialog]").fancybox();
|
||||||
}).fail(function(jqxhr, settings, exception) {
|
|
||||||
console.error("Failed to load Fancybox script: ", exception);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
resetFilters: resetFilters,
|
|
||||||
initFancybox: initFancybox
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
|
|
||||||
// --- Global Functions (for backward compatibility if needed, but prefer to call from Headphones object) ---
|
|
||||||
// These global functions would ideally be removed and replaced with direct calls to Headphones.API, Headphones.UI, etc.
|
|
||||||
// For a gradual transition, they can act as wrappers.
|
|
||||||
|
|
||||||
// Original getThumb / getImageLinks replacement
|
|
||||||
function getImageLinks(elem, id, type, unveil) {
|
|
||||||
Headphones.Images.load($(elem), id, type, unveil);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Original getInfo replacement
|
|
||||||
function getInfo(elem, id, type) {
|
|
||||||
Headphones.Images.getInfo($(elem), id, type);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Original doAjaxCall replacement (needs careful integration with new params)
|
|
||||||
// The HTML templates are passing doAjaxCall(url,elem,reload,form).
|
|
||||||
// This wrapper needs to convert those to the new options object.
|
|
||||||
function doAjaxCall(url, elem, reloadType, isFormSubmission) {
|
|
||||||
var options = {
|
|
||||||
url: url,
|
|
||||||
contextElement: elem,
|
|
||||||
reloadType: reloadType
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isFormSubmission) {
|
|
||||||
// This part needs careful migration. The original `doAjaxCall`
|
|
||||||
// had logic like `var formID = "#"+url; var dataString = $(formID).serialize();`
|
|
||||||
// and validation like `if ( $('td#select input[type=checkbox]').length > 0 && !$('td#select input[type=checkbox]').is(':checked') ... )`.
|
|
||||||
// This validation MUST be done *before* calling doAjaxCall in the specific HTML template's JS.
|
|
||||||
// Here, we assume `url` is the form ID if `isFormSubmission` is true.
|
|
||||||
var $form = $('#' + url); // Assuming url is the form ID
|
|
||||||
if ($form.length) {
|
|
||||||
options.data = $form.serialize();
|
|
||||||
} else {
|
|
||||||
console.warn("doAjaxCall: form with ID '" + url + "' not found for submission.");
|
|
||||||
// If the form cannot be found, it should likely be an error.
|
|
||||||
Headphones.UI.Message.show("Form not found for submission.", 'error');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Headphones.API.call(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
function doSimpleAjaxCall(url) {
|
|
||||||
Headphones.API.simpleCall(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetFilters(text) {
|
|
||||||
Headphones.Utils.resetFilters(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
function initFancybox() {
|
|
||||||
Headphones.Utils.initFancybox();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Placeholder for `initThisPage` - this function is typically defined per HTML template.
|
|
||||||
// It's called by `$(document).ready` in many templates.
|
|
||||||
// This should be removed from `headphones.js` and defined in each template.
|
|
||||||
// function initThisPage() { /* defined in each HTML template */ }
|
|
||||||
|
|
||||||
|
|
||||||
// --- Document Ready ---
|
|
||||||
$(document).ready(function(){
|
$(document).ready(function(){
|
||||||
Headphones.UI.Elements.initHeader(); // Initialize global header effects
|
initHeader();
|
||||||
Headphones.UI.Elements.initActions(); // Initialize global action buttons (jQuery UI)
|
|
||||||
|
|
||||||
// Ensure `initThisPage()` is called from the individual HTML template's ready handler.
|
|
||||||
// This `script.js` file should be included *before* the template's specific script block.
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,9 +6,8 @@
|
|||||||
<%def name="headerIncludes()">
|
<%def name="headerIncludes()">
|
||||||
<div id="subhead_container">
|
<div id="subhead_container">
|
||||||
<div id="subhead_menu">
|
<div id="subhead_menu">
|
||||||
<%-- Changed href to # and added data-action for JS handling --%>
|
<a class="menu_link_edit" href="clearLogs"><i class="fa fa-trash-o"></i> Clear log</a>
|
||||||
<a class="menu_link_action" href="#" data-action="clearLogs" data-success="Logs cleared successfully!"><i class="fa fa-trash-o"></i> Clear log</a>
|
<a class="menu_link_edit" href="toggleVerbose"><i class="fa fa-pencil"></i> Toggle Debug Log</a>
|
||||||
<a class="menu_link_action" href="#" data-action="toggleVerbose" data-success="Log verbosity toggled!"><i class="fa fa-pencil"></i> Toggle Debug Log</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</%def>
|
</%def>
|
||||||
@@ -20,18 +19,17 @@
|
|||||||
<table class="display" id="log_table">
|
<table class="display" id="log_table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="column-timestamp">Timestamp</th>
|
<th id="timestamp">Timestamp</th>
|
||||||
<th class="column-level">Level</th>
|
<th id="level">Level</th>
|
||||||
<th class="column-message">Message</th>
|
<th id="message">Message</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<br>
|
<br>
|
||||||
<div class="refresh-controls" align="center">
|
<div align="center">Refresh rate:
|
||||||
Refresh rate:
|
<select id="refreshrate" onchange="setRefresh()">
|
||||||
<select id="refreshrate"> <%-- Removed inline onchange --%>
|
|
||||||
<option value="0" selected="selected">No Refresh</option>
|
<option value="0" selected="selected">No Refresh</option>
|
||||||
<option value="5">5 Seconds</option>
|
<option value="5">5 Seconds</option>
|
||||||
<option value="15">15 Seconds</option>
|
<option value="15">15 Seconds</option>
|
||||||
@@ -39,62 +37,43 @@
|
|||||||
<option value="60">60 Seconds</option>
|
<option value="60">60 Seconds</option>
|
||||||
<option value="300">5 Minutes</option>
|
<option value="300">5 Minutes</option>
|
||||||
<option value="600">10 Minutes</option>
|
<option value="600">10 Minutes</option>
|
||||||
</select>
|
</select></div>
|
||||||
</div>
|
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="headIncludes()">
|
<%def name="headIncludes()">
|
||||||
${parent.headIncludes()} <%-- Ensure parent head includes are kept --%>
|
|
||||||
<link rel="stylesheet" href="interfaces/default/css/data_table.css">
|
<link rel="stylesheet" href="interfaces/default/css/data_table.css">
|
||||||
<style>
|
|
||||||
/* Example CSS for log levels */
|
|
||||||
.gradeX { /* For ERROR */
|
|
||||||
background-color: #fdd; /* Light red */
|
|
||||||
}
|
|
||||||
.gradeW { /* For WARNING */
|
|
||||||
background-color: #ffc; /* Light yellow */
|
|
||||||
}
|
|
||||||
.gradeZ { /* For INFO/DEBUG */
|
|
||||||
/* default background or light gray */
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="javascriptIncludes()">
|
<%def name="javascriptIncludes()">
|
||||||
${parent.javascriptIncludes()} <%-- Ensure parent javascript includes are kept --%>
|
|
||||||
<script src="js/libs/jquery.dataTables.min.js"></script>
|
<script src="js/libs/jquery.dataTables.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Encapsulate page-specific logic
|
$(document).ready(function() {
|
||||||
var LogsPage = LogsPage || {};
|
initActions();
|
||||||
|
|
||||||
LogsPage.timer = null; // To hold the interval timer ID
|
|
||||||
|
|
||||||
LogsPage.initDataTable = function() {
|
|
||||||
$('#log_table').dataTable( {
|
$('#log_table').dataTable( {
|
||||||
"bProcessing": true,
|
"bProcessing": true,
|
||||||
"bServerSide": true,
|
"bServerSide": true,
|
||||||
"sAjaxSource": 'getLog',
|
"sAjaxSource": 'getLog',
|
||||||
"sPaginationType": "full_numbers",
|
"sPaginationType": "full_numbers",
|
||||||
"aaSorting": [[0, 'desc']], // Sort by timestamp descending
|
"aaSorting": [[0, 'desc']],
|
||||||
"iDisplayLength": 25,
|
"iDisplayLength": 25,
|
||||||
"bStateSave": true, // Retain table state across page loads
|
"bStateSave": true,
|
||||||
"oLanguage": {
|
"oLanguage": {
|
||||||
"sSearch":"Filter:",
|
"sSearch":"Filter:",
|
||||||
"sLengthMenu":"Show _MENU_ lines per page",
|
"sLengthMenu":"Show _MENU_ lines per page",
|
||||||
"sEmptyTable": "No log information available",
|
"sEmptyTable": "No log information available",
|
||||||
"sInfo":"Showing _START_ to _END_ of _TOTAL_ lines",
|
"sInfo":"Showing _START_ to _END_ of _TOTAL_ lines",
|
||||||
"sInfoEmpty":"Showing 0 to 0 of 0 lines",
|
"sInfoEmpty":"Showing 0 to 0 of 0 lines",
|
||||||
"sInfoFiltered":"(filtered from _MAX_ total lines)"
|
"sInfoFiltered":"(filtered from _MAX_ total lines)"},
|
||||||
},
|
|
||||||
"fnRowCallback": function (nRow, aData, iDisplayIndex, iDisplayIndexFull) {
|
"fnRowCallback": function (nRow, aData, iDisplayIndex, iDisplayIndexFull) {
|
||||||
// aData[1] contains the 'Level' from the server
|
|
||||||
if (aData[1] === "ERROR") {
|
if (aData[1] === "ERROR") {
|
||||||
$(nRow).addClass("gradeX");
|
$('td', nRow).closest('tr').addClass("gradeX");
|
||||||
} else if (aData[1] === "WARNING") {
|
} else if (aData[1] === "WARNING") {
|
||||||
$(nRow).addClass("gradeW");
|
$('td', nRow).closest('tr').addClass("gradeW");
|
||||||
} else {
|
} else {
|
||||||
$(nRow).addClass("gradeZ");
|
$('td', nRow).closest('tr').addClass("gradeZ");
|
||||||
}
|
}
|
||||||
|
|
||||||
return nRow;
|
return nRow;
|
||||||
},
|
},
|
||||||
"fnDrawCallback": function (o) {
|
"fnDrawCallback": function (o) {
|
||||||
@@ -102,76 +81,30 @@
|
|||||||
$('html,body').scrollTop(0);
|
$('html,body').scrollTop(0);
|
||||||
},
|
},
|
||||||
"fnServerData": function ( sSource, aoData, fnCallback ) {
|
"fnServerData": function ( sSource, aoData, fnCallback ) {
|
||||||
// Custom function for fetching data, using $.getJSON
|
/* Add some extra data to the sender */
|
||||||
$.getJSON(sSource, aoData, function (json) {
|
$.getJSON(sSource, aoData, function (json) {
|
||||||
fnCallback(json);
|
fnCallback(json)
|
||||||
}).fail(function(jqXHR, textStatus, errorThrown) {
|
|
||||||
console.error("Error fetching log data:", textStatus, errorThrown);
|
|
||||||
// Optional: Display an error message to the user
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
LogsPage.setRefresh = function() {
|
|
||||||
var refreshrateSelect = document.getElementById('refreshrate');
|
|
||||||
if (refreshrateSelect != null) {
|
|
||||||
// Clear any existing timer
|
|
||||||
if (LogsPage.timer) {
|
|
||||||
clearInterval(LogsPage.timer);
|
|
||||||
}
|
|
||||||
|
|
||||||
var refreshValue = parseInt(refreshrateSelect.value, 10);
|
|
||||||
if (refreshValue !== 0) {
|
|
||||||
// Set a new interval to redraw the DataTable
|
|
||||||
LogsPage.timer = setInterval(function() {
|
|
||||||
$('#log_table').dataTable().fnDraw(false); // 'false' prevents resetting current page
|
|
||||||
}, 1000 * refreshValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
LogsPage.initActions = function() {
|
|
||||||
// Event listener for the refresh rate dropdown
|
|
||||||
$('#refreshrate').on('change', LogsPage.setRefresh);
|
|
||||||
|
|
||||||
// Event delegation for header action links
|
|
||||||
$('#subhead_menu').on('click', '.menu_link_action', function(e) {
|
|
||||||
e.preventDefault(); // Prevent default link behavior
|
|
||||||
var $this = $(this);
|
|
||||||
var action = $this.data('action'); // 'clearLogs' or 'toggleVerbose'
|
|
||||||
var successMsg = $this.data('success');
|
|
||||||
|
|
||||||
if (typeof doAjaxCall === 'function') {
|
|
||||||
// Assuming doAjaxCall handles the AJAX request and success/error messages
|
|
||||||
doAjaxCall('/' + action, $this, 'table', successMsg, function() {
|
|
||||||
// Callback after successful AJAX call for specific actions
|
|
||||||
if (action === 'clearLogs') {
|
|
||||||
// Redraw table after clearing logs to show empty state or refreshed data
|
|
||||||
$('#log_table').dataTable().fnDraw();
|
|
||||||
}
|
|
||||||
// No specific redraw needed for toggleVerbose unless it changes displayed logs
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.error("doAjaxCall function is not defined. Cannot perform log action.");
|
|
||||||
// Fallback to direct navigation if AJAX utility is missing
|
|
||||||
// window.location.href = '/' + action;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
$(document).ready(function() {
|
|
||||||
LogsPage.initDataTable();
|
|
||||||
LogsPage.initActions(); // Initialize new event handlers
|
|
||||||
|
|
||||||
// Initial call to set refresh based on default selected option
|
|
||||||
LogsPage.setRefresh();
|
|
||||||
|
|
||||||
// Ensure global initActions from common.js is called if it handles other site-wide actions
|
|
||||||
if (typeof initActions === 'function') {
|
|
||||||
initActions();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<script>
|
||||||
|
var timer;
|
||||||
|
function setRefresh()
|
||||||
|
{
|
||||||
|
refreshrate = document.getElementById('refreshrate');
|
||||||
|
if(refreshrate != null)
|
||||||
|
{
|
||||||
|
if(timer)
|
||||||
|
{
|
||||||
|
clearInterval(timer);
|
||||||
|
}
|
||||||
|
if(refreshrate.value != 0)
|
||||||
|
{
|
||||||
|
timer = setInterval("$('#log_table').dataTable().fnDraw()",1000*refreshrate.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|||||||
+113
-222
@@ -6,11 +6,9 @@
|
|||||||
<%def name="headerIncludes()">
|
<%def name="headerIncludes()">
|
||||||
<div id="subhead_container">
|
<div id="subhead_container">
|
||||||
<div id="subhead_menu">
|
<div id="subhead_menu">
|
||||||
<%-- Retained id="manage_albums" as it's a specific trigger for a dialog --%>
|
<a class="menu_link_edit" id="manage_albums" href="javascript:void(0)"><i class="fa fa-pencil"></i> Manage Albums</a>
|
||||||
<a class="menu_link_edit" id="manage_albums" href="#"><i class="fa fa-pencil"></i> Manage Albums</a>
|
<div id="dialog" title="Choose Album Filter" style="display:none" class="configtable">
|
||||||
<div id="dialog-manage-albums" title="Choose Album Filter" style="display:none" class="configtable">
|
|
||||||
<div class="links">
|
<div class="links">
|
||||||
<%-- Links within dialog changed to use data-status for filtering if AJAX is desired on manageAlbums page --%>
|
|
||||||
<a href="manageAlbums?Status=Downloaded"><i class="fa fa-check fa-fw"></i> Manage Downloaded Albums</a><br>
|
<a href="manageAlbums?Status=Downloaded"><i class="fa fa-check fa-fw"></i> Manage Downloaded Albums</a><br>
|
||||||
<a href="manageAlbums?Status=Skipped"><i class="fa fa-flag fa-fw"></i> Manage Skipped Albums</a><br>
|
<a href="manageAlbums?Status=Skipped"><i class="fa fa-flag fa-fw"></i> Manage Skipped Albums</a><br>
|
||||||
<a href="manageAlbums?Status=Snatched"><i class="fa fa-cloud-download fa-fw"></i> </span>Manage Snatched Albums</a><br>
|
<a href="manageAlbums?Status=Snatched"><i class="fa fa-cloud-download fa-fw"></i> </span>Manage Snatched Albums</a><br>
|
||||||
@@ -42,9 +40,8 @@
|
|||||||
<li><a href="#tabs-4">Force Legacy</a></li>
|
<li><a href="#tabs-4">Force Legacy</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<div id="tabs-1" class="configtable">
|
<div id="tabs-1" class="configtable">
|
||||||
<%-- action="musicScan" method="GET" is fine for form submission if full page reload is intended --%>
|
|
||||||
<form action="musicScan" method="GET" id="musicScan">
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
|
<form action="musicScan" method="GET" id="musicScan">
|
||||||
<legend>Scan Music Library</legend>
|
<legend>Scan Music Library</legend>
|
||||||
<p><strong>Where do you keep your music?</strong></p>
|
<p><strong>Where do you keep your music?</strong></p>
|
||||||
<p>You can put in any directory, and it will scan for audio files in that folder
|
<p>You can put in any directory, and it will scan for audio files in that folder
|
||||||
@@ -55,22 +52,25 @@
|
|||||||
</p>
|
</p>
|
||||||
<br/>
|
<br/>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label for="music_dir_path">Path to directory</label>
|
<label for="">Path to directory</label>
|
||||||
<%-- Using HTML5 placeholder attribute and proper ID --%>
|
%if headphones.CONFIG.MUSIC_DIR:
|
||||||
<input type="text" id="music_dir_path" value="${headphones.CONFIG.MUSIC_DIR or ''}" name="path" size="70" placeholder="Enter a Music Directory to scan" />
|
<input type="text" value="${headphones.CONFIG.MUSIC_DIR}" name="path" size="70" />
|
||||||
|
%else:
|
||||||
|
<input type="text" value="Enter a Music Directory to scan" onfocus="if
|
||||||
|
(this.value==this.defaultValue) this.value='';" name="path" size="70" />
|
||||||
|
%endif
|
||||||
</div>
|
</div>
|
||||||
<div class="row checkbox">
|
<div class="row checkbox">
|
||||||
<input type="checkbox" name="libraryscan" id="libraryscan" value="1" ${checked(headphones.CONFIG.LIBRARYSCAN)}><label for="libraryscan">Automatically scan library</label>
|
<input type="checkbox" name="libraryscan" id="libraryscan" value="1" ${checked(headphones.CONFIG.LIBRARYSCAN)}><label>Automatically scan library</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="row checkbox">
|
<div class="row checkbox">
|
||||||
<input type="checkbox" name="autoadd" id="autoadd" value="1" ${checked(headphones.CONFIG.AUTO_ADD_ARTISTS)}><label for="autoadd">Auto-add new artists</label>
|
<input type="checkbox" name="autoadd" id="autoadd" value="1" ${checked(headphones.CONFIG.AUTO_ADD_ARTISTS)}><label>Auto-add new artists</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<br>
|
<br>
|
||||||
<%-- Buttons use classes and data attributes for AJAX --%>
|
<input type="button" value="Save Changes and Scan" onclick="addScanAction();doAjaxCall('musicScan',$(this),'tabs',true);return false;" data-success="Changes saved. Library will be scanned">
|
||||||
<input type="button" class="ajax-button" data-action="musicScan" data-scan="1" data-success="Changes saved. Library will be scanned" value="Save Changes and Scan">
|
<input type="button" value="Save Changes without Scanning Library" onclick="doAjaxCall('musicScan',$(this),'tabs',true);return false;" data-success="Changes Saved Successfully">
|
||||||
<input type="button" class="ajax-button" data-action="musicScan" data-success="Changes Saved Successfully" value="Save Changes without Scanning Library">
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -81,15 +81,19 @@
|
|||||||
<p>Enter the username whose artists you want to import:</p>
|
<p>Enter the username whose artists you want to import:</p>
|
||||||
<br/>
|
<br/>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label for="lastfm_username">Username</label>
|
<label for="">Username</label>
|
||||||
<%-- Using HTML5 placeholder attribute --%>
|
<%
|
||||||
<input type="text" id="lastfm_username" value="${headphones.CONFIG.LASTFM_USERNAME or ''}" placeholder="Last.fm username" name="username" size="18" />
|
if headphones.CONFIG.LASTFM_USERNAME:
|
||||||
<%-- Changed to use class and data attributes for AJAX --%>
|
lastfmvalue = headphones.CONFIG.LASTFM_USERNAME
|
||||||
<a href="#" class="ajax-link" data-action="importLastFM" data-reset="username" data-success="Last.fm username has been reset"><i class="fa fa-reply"></i> Reset username</a>
|
else:
|
||||||
|
lastfmvalue = ''
|
||||||
|
%>
|
||||||
|
<input type="text" value="${lastfmvalue}" placeholder="Last.fm username" onfocus="if
|
||||||
|
(this.value==this.defaultValue) this.value='';" name="username" id="username" size="18" />
|
||||||
|
<a href="javascript:void(0)" onclick="doAjaxCall('importLastFM?username=',$(this),'tabs');return false;" data-success="Last.fm username has been reset"><i class="fa fa-reply"></i> Reset username</a>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<%-- Changed to use class and data attributes for AJAX --%>
|
<input type="button" value="Save changes" onclick="doAjaxCall('importLastFM',$(this),'tabs',true);return false;" data-success="Last.fm artists will be imported" data-error="Fill in a last.fm username"/>
|
||||||
<input type="button" class="ajax-button" data-action="importLastFM" data-success="Last.fm artists will be imported" data-error="Fill in a last.fm username" value="Save changes"/>
|
|
||||||
</form>
|
</form>
|
||||||
<br/>
|
<br/>
|
||||||
<form action="importLastFMTag" method="GET" id="importLastFMTag">
|
<form action="importLastFMTag" method="GET" id="importLastFMTag">
|
||||||
@@ -98,15 +102,16 @@
|
|||||||
<p>Enter tag from which you want import top artists:</p>
|
<p>Enter tag from which you want import top artists:</p>
|
||||||
<br/>
|
<br/>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label for="lastfm_tag">Tag</label>
|
<label>Tag</label>
|
||||||
<input type="text" id="lastfm_tag" value="" name="tag" size="18" placeholder="Enter tag"/>
|
<input type="text" value="" onfocus="if
|
||||||
|
(this.value==this.defaultValue) this.value='';" name="tag" id="tag" size="18" />
|
||||||
<br/>
|
<br/>
|
||||||
<label for="lastfm_limit">Limit</label>
|
<label>Limit</label>
|
||||||
<input type="text" id="lastfm_limit" value="50" name="limit" size="18" placeholder="50"/>
|
<input type="text" value="50" onfocus="if
|
||||||
|
(this.value==this.defaultValue) this.value='';" name="limit" id="limit" size="18" />
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<%-- Standard submit button for this form --%>
|
<input type="submit" />
|
||||||
<input type="submit" value="Import Tag"/>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -116,43 +121,40 @@
|
|||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Force Search</legend>
|
<legend>Force Search</legend>
|
||||||
<div class="links">
|
<div class="links">
|
||||||
<%-- All links use classes and data attributes for AJAX --%>
|
<a href="javascript:void(0)" onclick="doAjaxCall('forceSearch',$(this))" data-success="Checking for wanted albums successful" data-error="Error checking wanted albums"><i class="fa fa-search fa-fw"></i> Force Check for Wanted Albums</a>
|
||||||
<a href="#" class="ajax-link" data-action="forceSearch" data-success="Checking for wanted albums successful" data-error="Error checking wanted albums"><i class="fa fa-search fa-fw"></i> Force Check for Wanted Albums</a>
|
<a href="javascript:void(0)" onclick="doAjaxCall('forceUpdate',$(this))" data-success="Update active artists successful" data-error="Error forcing update artists"><i class="fa fa-heart fa-fw"></i> Force Update Active Artists [Fast]</a>
|
||||||
<a href="#" class="ajax-link" data-action="forceUpdate" data-success="Update active artists successful" data-error="Error forcing update artists"><i class="fa fa-heart fa-fw"></i> Force Update Active Artists [Fast]</a>
|
<a href="javascript:void(0)" onclick="doAjaxCall('checkGithub',$(this))" data-success="Checking for update successful" data-error="Error checking for update"><i class="fa fa-refresh fa-fw"></i> Check for Headphones Updates</a>
|
||||||
<a href="#" class="ajax-link" data-action="checkGithub" data-success="Checking for update successful" data-error="Error checking for update"><i class="fa fa-refresh fa-fw"></i> Check for Headphones Updates</a>
|
<a href="javascript:void(0)" id="delete_empty_artists"><i class="fa fa-trash-o fa-fw"></i> Delete empty Artists</a>
|
||||||
|
<div id="emptyartistdialog" title="Confirm Artist Deletion" style="display:none" class="configtable">
|
||||||
<a href="#" class="open-dialog-trigger" data-dialog-id="dialog-empty-artists"><i class="fa fa-trash-o fa-fw"></i> Delete empty Artists</a>
|
|
||||||
<div id="dialog-empty-artists" title="Confirm Artist Deletion" style="display:none" class="configtable">
|
|
||||||
%if emptyArtists:
|
%if emptyArtists:
|
||||||
<h3>The following artists will be deleted:</h3>
|
<h3>The following artists will be deleted:</h3>
|
||||||
|
|
||||||
%for emptyArtist in emptyArtists:
|
%for emptyArtist in emptyArtists:
|
||||||
<p>${emptyArtist['ArtistName']}</p>
|
<p>${emptyArtist['ArtistName']}</p>
|
||||||
%endfor
|
%endfor
|
||||||
<%-- Button uses class and data attributes for AJAX --%>
|
<input type="button" value="Delete Empty Artists" onclick="doAjaxCall('deleteEmptyArtists',$(this))" data-success="Empty Artists deleted" data-error="Error deleting empty artists">
|
||||||
<input type="button" class="ajax-button" data-action="deleteEmptyArtists" data-success="Empty Artists deleted" data-error="Error deleting empty artists" value="Delete Empty Artists">
|
|
||||||
%else:
|
%else:
|
||||||
<p>No empty artists found.</p>
|
No empty artists found.
|
||||||
%endif
|
%endif
|
||||||
</div>
|
</div>
|
||||||
<div id="post_process">
|
<div id="post_process">
|
||||||
<a href="#" class="open-dialog-trigger" data-dialog-id="dialog-post-process"><i class="fa fa-wrench fa-fw"></i> Force Post-Process Albums in Download Folder</a>
|
<a href="javascript:void(0)" class="btnOpenDialog"><i class="fa fa-wrench fa-fw"></i> Force Post-Process Albums in Download Folder</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<div class="row" id="post_process_alternate">
|
<div class="row" id="post_process_alternate">
|
||||||
<label for="alt_dir_path">Force Post-Process Albums in Alternate Folder</label>
|
<label>Force Post-Process Albums in Alternate Folder</label>
|
||||||
<input type="text" value="" name="dir" id="alt_dir_path" size="50" placeholder="Enter alternate directory" />
|
<input type="text" value="" name="dir" id="dir" size="50" />
|
||||||
<input type="button" class="open-dialog-trigger ajax-dialog-submit" data-dialog-id="dialog-post-process" data-input-id="alt_dir_path" data-input-param="dir" value="Submit" />
|
<input type="button" class="btnOpenDialog" value="Submit" />
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<div class="row" id="post_process_single">
|
<div class="row" id="post_process_single">
|
||||||
<label for="album_dir_path">Post-Process Single Folder</label>
|
<label>Post-Process Single Folder</label>
|
||||||
<input type="text" value="" name="album_dir" id="album_dir_path" size="50" placeholder="Enter album directory" />
|
<input type="text" value="" name="album_dir" id="album_dir" size="50" />
|
||||||
<input type="button" class="open-dialog-trigger ajax-dialog-submit" data-dialog-id="dialog-post-process" data-input-id="album_dir_path" data-input-param="album_dir" value="Submit" />
|
<input type="button" class="btnOpenDialog" value="Submit" />
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
@@ -164,200 +166,89 @@
|
|||||||
<legend>Force Legacy</legend>
|
<legend>Force Legacy</legend>
|
||||||
<p>Please note that these functions will take a significant amount of time to complete.</p>
|
<p>Please note that these functions will take a significant amount of time to complete.</p>
|
||||||
<div class="links">
|
<div class="links">
|
||||||
<%-- All links use classes and data attributes for AJAX --%>
|
<a href="javascript:void(0)" onclick="doAjaxCall('forceFullUpdate',$(this))" data-success="Update active artists successful" data-error="Error forcing update artists"><i class="fa fa-heart fa-fw"></i> Force Update Active Artists [Comprehensive]</a>
|
||||||
<a href="#" class="ajax-link" data-action="forceFullUpdate" data-success="Update active artists successful" data-error="Error forcing update artists"><i class="fa fa-heart fa-fw"></i> Force Update Active Artists [Comprehensive]</a>
|
|
||||||
<BR>
|
<BR>
|
||||||
<a href="#" class="ajax-link" data-action="forceScan" data-success="Library scan successful" data-error="Error forcing library scan"><i class="fa fa-refresh fa-fw"></i> Force Re-scan Library [Comprehensive]</a>
|
<a href="javascript:void(0)" onclick="doAjaxCall('forceScan',$(this))" data-success="Library scan successful" data-error="Error forcing library scan"><i class="fa fa-refresh fa-fw"></i> Force Re-scan Library [Comprehensive]</a>
|
||||||
<BR>
|
<BR>
|
||||||
<small>*Warning: If you choose [Force Re-scan Library], any manually ignored/matched artists/albums will be reset to "unmatched".</small>
|
<small>*Warning: If you choose [Force Re-scan Library], any manually ignored/matched artists/albums will be reset to "unmatched".</small>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
|
||||||
|
|
||||||
<%-- Shared dialog for post-process confirmation --%>
|
|
||||||
<div id="dialog-post-process" title="Keep original folder(s)?" style="display:none">
|
|
||||||
<p>Do you want to keep the original folder(s) after post-processing? If you click no, the folders still might be kept depending on your global settings</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div id="dialog-confirm"></div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</%def>
|
</%def>
|
||||||
<%def name="javascriptIncludes()">
|
<%def name="javascriptIncludes()">
|
||||||
${parent.javascriptIncludes()} <%-- Ensure parent javascript includes are kept --%>
|
|
||||||
<script>
|
<script>
|
||||||
// Encapsulate page-specific logic
|
function addScanAction() {
|
||||||
var ManagePage = ManagePage || {};
|
$('#autoadd').append('<input type="hidden" name="scan" value=1 />');
|
||||||
|
|
||||||
ManagePage.init = function() {
|
|
||||||
// Initialize jQuery UI Tabs
|
|
||||||
jQuery( "#tabs" ).tabs();
|
|
||||||
|
|
||||||
// Initialize jQuery UI Dialogs
|
|
||||||
$('#dialog-manage-albums').dialog({
|
|
||||||
autoOpen: false,
|
|
||||||
modal: true,
|
|
||||||
width: 450,
|
|
||||||
height: 'auto',
|
|
||||||
title: "Choose Album Filter"
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#dialog-empty-artists').dialog({
|
|
||||||
autoOpen: false,
|
|
||||||
modal: true,
|
|
||||||
width: 500,
|
|
||||||
height: 'auto',
|
|
||||||
title: "Confirm Artist Deletion"
|
|
||||||
});
|
|
||||||
|
|
||||||
// Post-process confirmation dialog
|
|
||||||
$('#dialog-post-process').dialog({
|
|
||||||
autoOpen: false,
|
|
||||||
resizable: false,
|
|
||||||
modal: true,
|
|
||||||
height: 170,
|
|
||||||
width: 400,
|
|
||||||
title: "Keep original folder(s)?",
|
|
||||||
buttons: {
|
|
||||||
"Yes": function () {
|
|
||||||
var $dialog = $(this);
|
|
||||||
$dialog.dialog('close');
|
|
||||||
// Retrieve parameters stored on the trigger element
|
|
||||||
var $trigger = $dialog.data('triggerElement');
|
|
||||||
var url = $trigger.data('ajax-url') + "&keep_original_folder=True";
|
|
||||||
var successMsg = $trigger.data('success') || "Post-Processor is being loaded";
|
|
||||||
var errorMsg = $trigger.data('error') || "Error during Post-Processing";
|
|
||||||
|
|
||||||
if (typeof doAjaxCall === 'function') {
|
|
||||||
doAjaxCall(url, $trigger, 'tabs', successMsg, errorMsg);
|
|
||||||
} else {
|
|
||||||
console.error("doAjaxCall function is not defined. Cannot perform post-process action.");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"No": function () {
|
|
||||||
var $dialog = $(this);
|
|
||||||
$dialog.dialog('close');
|
|
||||||
// Retrieve parameters stored on the trigger element
|
|
||||||
var $trigger = $dialog.data('triggerElement');
|
|
||||||
var url = $trigger.data('ajax-url') + "&keep_original_folder=False";
|
|
||||||
var successMsg = $trigger.data('success') || "Post-Processor is being loaded";
|
|
||||||
var errorMsg = $trigger.data('error') || "Error during Post-Processing";
|
|
||||||
|
|
||||||
if (typeof doAjaxCall === 'function') {
|
|
||||||
doAjaxCall(url, $trigger, 'tabs', successMsg, errorMsg);
|
|
||||||
} else {
|
|
||||||
console.error("doAjaxCall function is not defined. Cannot perform post-process action.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Event delegation for opening dialogs
|
|
||||||
$(document).on('click', '.open-dialog-trigger', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
var dialogId = $(this).data('dialog-id');
|
|
||||||
var $dialog = $('#' + dialogId);
|
|
||||||
|
|
||||||
// For post-process dialogs, build the base URL and store it
|
|
||||||
if (dialogId === 'dialog-post-process') {
|
|
||||||
var url = "forcePostProcess?";
|
|
||||||
var inputId = $(this).data('input-id');
|
|
||||||
var inputParam = $(this).data('input-param');
|
|
||||||
if (inputId && inputParam) {
|
|
||||||
var inputValue = $('#' + inputId).val();
|
|
||||||
if (inputValue) {
|
|
||||||
url += inputParam + "=" + encodeURIComponent(inputValue) + "&";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Store the base URL and the triggering element on the dialog for use in buttons
|
|
||||||
$dialog.data('ajax-url', url);
|
|
||||||
$dialog.data('triggerElement', $(this));
|
|
||||||
}
|
|
||||||
$dialog.dialog('open');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Event delegation for general AJAX buttons (Save Changes, Delete Empty Artists)
|
|
||||||
$(document).on('click', '.ajax-button', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
var $this = $(this);
|
|
||||||
var action = $this.data('action'); // e.g., musicScan, deleteEmptyArtists
|
|
||||||
var successMsg = $this.data('success');
|
|
||||||
var errorMsg = $this.data('error');
|
|
||||||
var formId = $this.closest('form').attr('id');
|
|
||||||
var url = '/' + action;
|
|
||||||
var data = {};
|
|
||||||
|
|
||||||
// For form submissions, serialize the form data
|
|
||||||
if (formId) {
|
|
||||||
var formData = $('#' + formId).serializeArray();
|
|
||||||
$.each(formData, function() {
|
|
||||||
if (data[this.name]) {
|
|
||||||
if (!data[this.name].push) {
|
|
||||||
data[this.name] = [data[this.name]];
|
|
||||||
}
|
|
||||||
data[this.name].push(this.value || '');
|
|
||||||
} else {
|
|
||||||
data[this.name] = this.value || '';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add specific data attributes from the button if present
|
|
||||||
$.each($this.data(), function(key, value) {
|
|
||||||
// Exclude 'action', 'success', 'error', 'scan' (handled specifically)
|
|
||||||
if (key !== 'action' && key !== 'success' && key !== 'error' && key !== 'scan') {
|
|
||||||
data[key] = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Special handling for 'scan' parameter for musicScan
|
|
||||||
if ($this.data('scan') === 1) {
|
|
||||||
data['scan'] = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special handling for 'reset' parameter for importLastFM
|
|
||||||
if ($this.data('reset') === 'username') {
|
|
||||||
url += '?username='; // Append empty username to clear it
|
|
||||||
data = {}; // Clear other data if resetting
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof doAjaxCall === 'function') {
|
|
||||||
// Assuming doAjaxCall takes URL, trigger, context, successMsg, errorMsg, and data
|
|
||||||
doAjaxCall(url, $this, 'tabs', successMsg, errorMsg, data);
|
|
||||||
} else {
|
|
||||||
console.error("doAjaxCall function is not defined. Cannot perform AJAX button action.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it's a delete artists action, close the dialog
|
|
||||||
if (action === 'deleteEmptyArtists') {
|
|
||||||
$('#dialog-empty-artists').dialog('close');
|
|
||||||
// Optionally, refresh the list of empty artists if it's dynamic
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Event delegation for general AJAX links (Force Actions, Force Legacy, Reset Last.fm)
|
|
||||||
$(document).on('click', '.ajax-link', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
var $this = $(this);
|
|
||||||
var action = $this.data('action');
|
|
||||||
var successMsg = $this.data('success');
|
|
||||||
var errorMsg = $this.data('error');
|
|
||||||
var url = '/' + action; // Base URL
|
|
||||||
|
|
||||||
if (typeof doAjaxCall === 'function') {
|
|
||||||
doAjaxCall(url, $this, null, successMsg, errorMsg); // No specific context needed like 'tabs' here unless doAjaxCall uses it.
|
|
||||||
} else {
|
|
||||||
console.error("doAjaxCall function is not defined. Cannot perform AJAX link action.");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Call initActions if it's a global function that needs to run
|
|
||||||
if (typeof initActions === 'function') {
|
|
||||||
initActions();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function fnOpenNormalDialog(id) {
|
||||||
|
if (id === "post_process"){
|
||||||
|
var url = "forcePostProcess?"
|
||||||
|
}
|
||||||
|
if (id === "post_process_alternate"){
|
||||||
|
var dir = $('#dir').val();
|
||||||
|
var url = "forcePostProcess?dir=" + dir + "&"
|
||||||
|
}
|
||||||
|
if (id === "post_process_single"){
|
||||||
|
var dir = $('#album_dir').val();
|
||||||
|
var url = "forcePostProcess?album_dir=" + dir + "&"
|
||||||
|
}
|
||||||
|
var t = $('<a data-success="Post-Processor is being loaded" data-error="Error during Post-Processing">');
|
||||||
|
$("#dialog-confirm").html("Do you want to keep the original folder(s) after post-processing? If you click no, the folders still might be kept depending on your global settings");
|
||||||
|
|
||||||
|
// Define the Dialog and its properties.
|
||||||
|
$("#dialog-confirm").dialog({
|
||||||
|
resizable: false,
|
||||||
|
modal: true,
|
||||||
|
title: "Keep original folder(s)?",
|
||||||
|
height: 170,
|
||||||
|
width: 400,
|
||||||
|
buttons: {
|
||||||
|
"Yes": function () {
|
||||||
|
$(this).dialog('close');
|
||||||
|
doAjaxCall(url + "keep_original_folder=True", t);
|
||||||
|
},
|
||||||
|
"No": function () {
|
||||||
|
$(this).dialog('close');
|
||||||
|
doAjaxCall(url + "keep_original_folder=False", t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$('.btnOpenDialog').click(function(e){
|
||||||
|
e.preventDefault();
|
||||||
|
var parentId = $(this).closest('div').prop('id');
|
||||||
|
fnOpenNormalDialog(parentId);
|
||||||
|
});
|
||||||
|
|
||||||
|
function callback(value) {
|
||||||
|
if (value) {
|
||||||
|
alert("Confirmed");
|
||||||
|
} else {
|
||||||
|
alert("Rejected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initThisPage() {
|
||||||
|
$('#manage_albums').click(function() {
|
||||||
|
$('#dialog').dialog();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
$('#delete_empty_artists').click(function() {
|
||||||
|
$('#emptyartistdialog').dialog();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
jQuery( "#tabs" ).tabs();
|
||||||
|
initActions();
|
||||||
|
};
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
ManagePage.init();
|
initThisPage();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<%inherit file="base.html" />
|
<%inherit file="base.html" />
|
||||||
<%!
|
<%!
|
||||||
# Removed direct DB imports/interactions here, as data should be pre-fetched server-side.
|
from headphones import db
|
||||||
# from headphones import db
|
import headphones
|
||||||
import headphones # Still needed for headphones.LOSSY_MEDIA_FORMATS if used
|
|
||||||
%>
|
%>
|
||||||
|
|
||||||
<%def name="headerIncludes()">
|
<%def name="headerIncludes()">
|
||||||
@@ -18,10 +17,9 @@
|
|||||||
<div id="manageheader" class="title">
|
<div id="manageheader" class="title">
|
||||||
<h1 class="clearfix"><i class="fa fa-music"></i> Manage Albums</h1>
|
<h1 class="clearfix"><i class="fa fa-music"></i> Manage Albums</h1>
|
||||||
</div>
|
</div>
|
||||||
<form action="markAlbums" method="get" id="markAlbumsForm"> <%-- Renamed ID to avoid conflict with markalbum div --%>
|
<form action="markAlbums" method="get" id="markAlbums">
|
||||||
<div id="markalbum">Mark selected albums as
|
<div id="markalbum">Mark selected albums as
|
||||||
<%-- Replaced inline onChange with a class and data attributes for JS handling --%>
|
<select name="action" onChange="doAjaxCall('markAlbums',$(this),'table',true);" data-error="You didn't select any albums">
|
||||||
<select name="action" id="markAlbumActionSelect">
|
|
||||||
<option disabled="disabled" selected="selected">Choose...</option>
|
<option disabled="disabled" selected="selected">Choose...</option>
|
||||||
<option value="Wanted">Wanted</option>
|
<option value="Wanted">Wanted</option>
|
||||||
<option value="WantedNew">Wanted (new only)</option>
|
<option value="WantedNew">Wanted (new only)</option>
|
||||||
@@ -30,27 +28,25 @@
|
|||||||
<option value="Ignored">Ignored</option>
|
<option value="Ignored">Ignored</option>
|
||||||
<option value="Downloaded">Downloaded</option>
|
<option value="Downloaded">Downloaded</option>
|
||||||
</select>
|
</select>
|
||||||
<input type="hidden" value="Go"> <%-- This hidden input might be redundant if data is sent via AJAX --%>
|
<input type="hidden" value="Go">
|
||||||
</div>
|
</div>
|
||||||
<table class="display" id="album_table">
|
<table class="display" id="album_table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="select-all-checkbox"><input type="checkbox" id="toggleAllAlbums" /></th> <%-- Added ID for easier targeting --%>
|
<th id="select"><input type="checkbox" onClick="toggle(this)" /></th>
|
||||||
<th class="column-albumname">Album</th>
|
<th id="albumname">Album</th>
|
||||||
<th class="column-artistname">Artist</th>
|
<th id="artistname">Artist</th>
|
||||||
<th class="column-reldate">Date</th>
|
<th id="reldate">Date</th>
|
||||||
<th class="column-type">Type</th>
|
<th id="type">Type</th>
|
||||||
<th class="column-status">Status</th>
|
<th id="status">Status</th>
|
||||||
<th class="column-have">Have</th>
|
<th id="have">Have</th>
|
||||||
<th class="column-bitrate">Bitrate</th>
|
<th id="bitrate">Bitrate</th>
|
||||||
<th class="column-albumformat">Format</th>
|
<th id="albumformat">Format</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
%for album in albums:
|
%for album in albums:
|
||||||
<%
|
<%
|
||||||
# Assuming these values are now pre-calculated and available in the 'album' dict
|
|
||||||
# Removed all in-template database queries for performance.
|
|
||||||
if album['Status'] == 'Skipped':
|
if album['Status'] == 'Skipped':
|
||||||
grade = 'Z'
|
grade = 'Z'
|
||||||
elif album['Status'] == 'Wanted':
|
elif album['Status'] == 'Wanted':
|
||||||
@@ -62,30 +58,45 @@
|
|||||||
else:
|
else:
|
||||||
grade = 'A'
|
grade = 'A'
|
||||||
|
|
||||||
# Use the pre-calculated values from the album object
|
myDB = db.DBConnection()
|
||||||
totaltracks_display = album.get('TotalTracks', '?')
|
totaltracks = len(myDB.select('SELECT TrackTitle from tracks WHERE AlbumID=?', [album['AlbumID']]))
|
||||||
havetracks_display = album.get('HaveTracks', 0)
|
havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE AlbumID=? AND Location IS NOT NULL', [album['AlbumID']])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ? AND AlbumTitle LIKE ? AND Matched = "Failed"', [album['ArtistName'], album['AlbumTitle']]))
|
||||||
percent_display = album.get('PercentOwned', 0)
|
|
||||||
bitrate_display = album.get('BitrateDisplay', '') # e.g., '192 kbps'
|
try:
|
||||||
albumformat_display = album.get('AlbumFormat', '') # e.g., 'MP3', 'FLAC', 'Mixed'
|
percent = (havetracks*100.0)/totaltracks
|
||||||
|
if percent > 100:
|
||||||
|
percent = 100
|
||||||
|
except (ZeroDivisionError, TypeError):
|
||||||
|
percent = 0
|
||||||
|
totaltracks = '?'
|
||||||
|
|
||||||
|
avgbitrate = myDB.action("SELECT AVG(BitRate) FROM tracks WHERE AlbumID=?", [album['AlbumID']]).fetchone()[0]
|
||||||
|
if avgbitrate:
|
||||||
|
bitrate = str(int(avgbitrate)/1000) + ' kbps'
|
||||||
|
else:
|
||||||
|
bitrate = ''
|
||||||
|
|
||||||
|
albumformatcount = myDB.action("SELECT COUNT(DISTINCT Format) FROM tracks WHERE AlbumID=?", [album['AlbumID']]).fetchone()[0]
|
||||||
|
if albumformatcount == 1:
|
||||||
|
albumformat = myDB.action("SELECT DISTINCT Format FROM tracks WHERE AlbumID=?", [album['AlbumID']]).fetchone()[0]
|
||||||
|
elif albumformatcount > 1:
|
||||||
|
albumformat = 'Mixed'
|
||||||
|
else:
|
||||||
|
albumformat = ''
|
||||||
|
|
||||||
|
lossy_formats = [str.upper(fmt) for fmt in headphones.LOSSY_MEDIA_FORMATS]
|
||||||
|
|
||||||
%>
|
%>
|
||||||
<tr class="grade${grade}">
|
<tr class="grade${grade}">
|
||||||
<td class="select-album-checkbox"><input type="checkbox" name="albumid_${album['AlbumID']}" value="${album['AlbumID']}" class="album-checkbox" /></td> <%-- Unique name and class for individual checkboxes --%>
|
<td id="select"><input type="checkbox" name="${album['AlbumID']}" class="checkbox" /></td>
|
||||||
<td class="column-albumname"><a href="albumPage?AlbumID=${album['AlbumID']}">${album['AlbumTitle']}</a></td>
|
<td id="albumname"><a href="albumPage?AlbumID=${album['AlbumID']}">${album['AlbumTitle']}</a></td>
|
||||||
<td class="column-artistname"><a href="artistPage?ArtistID=${album['ArtistID']}">${album['ArtistName']}</a></td>
|
<td id="artistname"><a href="artistPage?ArtistID=${album['ArtistID']}">${album['ArtistName']}</a></td>
|
||||||
<td class="column-reldate">${album['ReleaseDate']}</td>
|
<td id="reldate">${album['ReleaseDate']}</td>
|
||||||
<td class="column-type">${album['Type']}</td>
|
<td id="type">${album['Type']}</td>
|
||||||
<td class="column-status">${album['Status']}</td>
|
<td id="status">${album['Status']}</td>
|
||||||
<td class="column-have">
|
<td id="have"><span title="${percent}"><span><div class="progress-container"><div style="width:${percent}%"><div class="havetracks">${havetracks}/${totaltracks}</div></div></div></td>
|
||||||
<span title="${percent_display}"></span> <%-- Using percent_display for title attribute --%>
|
<td id="bitrate">${bitrate}</td>
|
||||||
<div class="progress-container" role="progressbar" aria-valuenow="${percent_display | int}" aria-valuemin="0" aria-valuemax="100">
|
<td id="albumformat">${albumformat}</td>
|
||||||
<div style="width:${percent_display}%">
|
|
||||||
<div class="havetracks">${havetracks_display}/${totaltracks_display}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="column-bitrate">${bitrate_display}</td>
|
|
||||||
<td class="column-albumformat">${albumformat_display}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
%endfor
|
%endfor
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -95,75 +106,28 @@
|
|||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="headIncludes()">
|
<%def name="headIncludes()">
|
||||||
${parent.headIncludes()} <%-- Ensure parent head includes are kept --%>
|
|
||||||
<link rel="stylesheet" href="interfaces/default/css/data_table.css">
|
<link rel="stylesheet" href="interfaces/default/css/data_table.css">
|
||||||
<style>
|
|
||||||
/* Styles for progress bar, similar to index.html for consistency */
|
|
||||||
.progress-container {
|
|
||||||
background-color: #eee;
|
|
||||||
border-radius: 5px;
|
|
||||||
height: 15px; /* Adjust height as needed */
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
width: 100%; /* Or a fixed width if preferred */
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-container > div {
|
|
||||||
background-color: #4CAF50; /* Green color for progress */
|
|
||||||
height: 100%;
|
|
||||||
border-radius: 5px;
|
|
||||||
text-align: center;
|
|
||||||
color: white;
|
|
||||||
font-size: 10px; /* Smaller font for percentage */
|
|
||||||
line-height: 15px; /* Vertically align text */
|
|
||||||
}
|
|
||||||
|
|
||||||
.havetracks {
|
|
||||||
padding: 0 5px; /* Add some padding around the text */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Styles for album status grades */
|
|
||||||
.gradeZ { /* Skipped */
|
|
||||||
background-color: #f2dede; /* Light red/pink */
|
|
||||||
}
|
|
||||||
.gradeX { /* Wanted */
|
|
||||||
background-color: #d9edf7; /* Light blue */
|
|
||||||
}
|
|
||||||
.gradeI { /* Ignored */
|
|
||||||
background-color: #fcf8e3; /* Light yellow */
|
|
||||||
}
|
|
||||||
.gradeC { /* Snatched */
|
|
||||||
background-color: #dff0d8; /* Light green */
|
|
||||||
}
|
|
||||||
.gradeA { /* Downloaded/Processed (default) */
|
|
||||||
/* No specific background or subtle light grey */
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="javascriptIncludes()">
|
<%def name="javascriptIncludes()">
|
||||||
${parent.javascriptIncludes()} <%-- Ensure parent javascript includes are kept --%>
|
|
||||||
<script src="js/libs/jquery.dataTables.min.js"></script>
|
<script src="js/libs/jquery.dataTables.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Encapsulate page-specific logic
|
function initThisPage() {
|
||||||
var ManageAlbumsPage = ManageAlbumsPage || {};
|
|
||||||
|
|
||||||
ManageAlbumsPage.initDataTable = function() {
|
|
||||||
$('#album_table').dataTable({
|
$('#album_table').dataTable({
|
||||||
"bDestroy": true,
|
"bDestroy": true,
|
||||||
"aoColumns": [
|
"aoColumns": [
|
||||||
null, // Checkbox column (not sortable)
|
null,
|
||||||
null, // Album Name
|
null,
|
||||||
null, // Artist Name
|
null,
|
||||||
null, // Date
|
null,
|
||||||
null, // Type
|
null,
|
||||||
null, // Status
|
null,
|
||||||
{ "sType": "title-numeric"}, // Have (uses title for numeric sort)
|
{ "sType": "title-numeric"},
|
||||||
null, // Bitrate
|
null,
|
||||||
null // Format
|
null
|
||||||
],
|
],
|
||||||
"aoColumnDefs": [
|
"aoColumnDefs": [
|
||||||
{ 'bSortable': false, 'aTargets': [ 0 ] } // Disable sorting for checkbox column
|
{ 'bSortable': false, 'aTargets': [ 0 ] }
|
||||||
],
|
],
|
||||||
"oLanguage": {
|
"oLanguage": {
|
||||||
"sLengthMenu":"Show _MENU_ albums per page",
|
"sLengthMenu":"Show _MENU_ albums per page",
|
||||||
@@ -172,83 +136,19 @@
|
|||||||
"sInfoEmpty":"Showing 0 to 0 of 0 albums",
|
"sInfoEmpty":"Showing 0 to 0 of 0 albums",
|
||||||
"sInfoFiltered":"(filtered from _MAX_ total albums)",
|
"sInfoFiltered":"(filtered from _MAX_ total albums)",
|
||||||
"sSearch": ""},
|
"sSearch": ""},
|
||||||
"bPaginate": false, // All data loaded on one page
|
"bPaginate": false,
|
||||||
"aaSorting": [[5, 'desc']], // Default sort by Status descending
|
"aaSorting": [[5, 'desc']],
|
||||||
"fnDrawCallback": function (o) {
|
"fnDrawCallback": function (o) {
|
||||||
// Jump to top of page
|
// Jump to top of page
|
||||||
$('html,body').scrollTop(0);
|
$('html,body').scrollTop(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
ManageAlbumsPage.initActions = function() {
|
|
||||||
// Event listener for the "Mark selected albums as" dropdown
|
|
||||||
$('#markAlbumActionSelect').on('change', function() {
|
|
||||||
var $this = $(this);
|
|
||||||
var actionValue = $this.val();
|
|
||||||
var selectedAlbumIds = [];
|
|
||||||
|
|
||||||
// Get all checked album checkboxes
|
|
||||||
$('.album-checkbox:checked').each(function() {
|
|
||||||
selectedAlbumIds.push($(this).val());
|
|
||||||
});
|
|
||||||
|
|
||||||
if (selectedAlbumIds.length === 0) {
|
|
||||||
// Display error if no albums are selected
|
|
||||||
if (typeof showMessage === 'function') { // Assuming showMessage is a global utility
|
|
||||||
showMessage($this.data('error') || "You didn't select any albums", 'error');
|
|
||||||
} else {
|
|
||||||
alert($this.data('error') || "You didn't select any albums");
|
|
||||||
}
|
|
||||||
// Reset select box to default
|
|
||||||
$this.val($('option:first', $this).val());
|
|
||||||
return; // Stop execution
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct URL with selected album IDs and action
|
|
||||||
var url = 'markAlbums?action=' + encodeURIComponent(actionValue);
|
|
||||||
$.each(selectedAlbumIds, function(index, id) {
|
|
||||||
url += '&albumid=' + encodeURIComponent(id); // Use a consistent param name if backend expects multiple
|
|
||||||
});
|
|
||||||
|
|
||||||
// Perform AJAX call
|
|
||||||
if (typeof doAjaxCall === 'function') {
|
|
||||||
doAjaxCall(url, $this, 'table', 'Albums marked as ' + actionValue + ' successfully!', 'Error marking albums.');
|
|
||||||
// After successful action, you might want to redraw the table or update rows
|
|
||||||
// $('#album_table').dataTable().fnDraw();
|
|
||||||
} else {
|
|
||||||
console.error("doAjaxCall function is not defined. Cannot mark albums asynchronously.");
|
|
||||||
}
|
|
||||||
// Reset select box to default after action
|
|
||||||
$this.val($('option:first', $this).val());
|
|
||||||
});
|
|
||||||
|
|
||||||
// Event listener for the "toggle all" checkbox
|
|
||||||
$('#toggleAllAlbums').on('click', function() {
|
|
||||||
$('.album-checkbox').prop('checked', this.checked);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Optionally, reset individual checkboxes if header checkbox is unchecked (and vice-versa if needed)
|
|
||||||
$('.album-checkbox').on('click', function() {
|
|
||||||
if (!this.checked) {
|
|
||||||
$('#toggleAllAlbums').prop('checked', false);
|
|
||||||
} else {
|
|
||||||
// If all are checked, check the header checkbox
|
|
||||||
if ($('.album-checkbox:checked').length === $('.album-checkbox').length) {
|
|
||||||
$('#toggleAllAlbums').prop('checked', true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$(document).ready(function() {
|
|
||||||
ManageAlbumsPage.initDataTable();
|
|
||||||
ManageAlbumsPage.initActions();
|
|
||||||
|
|
||||||
// Assuming resetFilters is a global function from common.js
|
|
||||||
if (typeof resetFilters === 'function') {
|
|
||||||
resetFilters("albums");
|
resetFilters("albums");
|
||||||
}
|
}
|
||||||
|
$(document).ready(function() {
|
||||||
|
initThisPage();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<%inherit file="base.html" />
|
<%inherit file="base.html" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<%def name="headerIncludes()">
|
<%def name="headerIncludes()">
|
||||||
<div id="subhead_container">
|
<div id="subhead_container">
|
||||||
|
|
||||||
@@ -13,10 +15,9 @@
|
|||||||
<div id="manageheader" class="title">
|
<div id="manageheader" class="title">
|
||||||
<h1 class="clearfix"><i class="fa fa-music"></i> Manage Artists</h1>
|
<h1 class="clearfix"><i class="fa fa-music"></i> Manage Artists</h1>
|
||||||
</div>
|
</div>
|
||||||
<form action="markArtists" method="get" id="markArtistsForm"> <%-- Renamed ID to avoid conflict with markalbum div --%>
|
<form action="markArtists" method="get" id="markArtists">
|
||||||
<div id="markartists_controls"> <%-- More descriptive ID for the controls div --%>
|
<div id="markalbum">
|
||||||
<%-- Replaced inline onChange with an ID for JS handling --%>
|
<select name="action" onChange="doAjaxCall('markArtists',$(this),'table',true);" data-error="You didn't select any artists">
|
||||||
<select name="action" id="markArtistActionSelect">
|
|
||||||
<option disabled="disabled" selected="selected">Choose...</option>
|
<option disabled="disabled" selected="selected">Choose...</option>
|
||||||
<option value="pause">Pause</option>
|
<option value="pause">Pause</option>
|
||||||
<option value="resume">Resume</option>
|
<option value="resume">Resume</option>
|
||||||
@@ -24,17 +25,17 @@
|
|||||||
<option value="delete">Delete</option>
|
<option value="delete">Delete</option>
|
||||||
</select>
|
</select>
|
||||||
selected artists
|
selected artists
|
||||||
<input type="hidden" value="Go"> <%-- This hidden input might be redundant if data is sent via AJAX --%>
|
<input type="hidden" value="Go">
|
||||||
</div>
|
</div>
|
||||||
<table class="display" id="artist_table">
|
<table class="display" id="artist_table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="select-all-checkbox"><input type="checkbox" id="toggleAllArtists" /></th> <%-- Added ID for easier targeting --%>
|
<th id="select"><input type="checkbox" onClick="toggle(this)" /></th>
|
||||||
<th class="column-albumart"></th> <%-- Changed ID to class for consistency and uniqueness --%>
|
<th id="albumart"></th>
|
||||||
<th class="column-name">Artist Name</th>
|
<th id="name">Artist Name</th>
|
||||||
<th class="column-status">Status</th>
|
<th id="status">Status</th>
|
||||||
<th class="column-album">Latest Album</th>
|
<th id="album">Latest Album</th>
|
||||||
<th class="column-lastupdated">Last Updated</th>
|
<th id="lastupdated">Last Updated</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -47,7 +48,6 @@
|
|||||||
else:
|
else:
|
||||||
grade = 'Z'
|
grade = 'Z'
|
||||||
|
|
||||||
# Python logic for display formatting is fine here, as it's not doing DB queries.
|
|
||||||
if artist['ReleaseDate'] and artist['LatestAlbum']:
|
if artist['ReleaseDate'] and artist['LatestAlbum']:
|
||||||
releasedate = artist['ReleaseDate']
|
releasedate = artist['ReleaseDate']
|
||||||
albumdisplay = '<i>%s</i> (%s)' % (artist['LatestAlbum'], artist['ReleaseDate'])
|
albumdisplay = '<i>%s</i> (%s)' % (artist['LatestAlbum'], artist['ReleaseDate'])
|
||||||
@@ -65,17 +65,12 @@
|
|||||||
|
|
||||||
%>
|
%>
|
||||||
<tr class="grade${grade}">
|
<tr class="grade${grade}">
|
||||||
<td class="select-artist-checkbox"><input type="checkbox" name="artistid_${artist['ArtistID']}" value="${artist['ArtistID']}" class="artist-checkbox" /></td> <%-- Unique name and class for individual checkboxes --%>
|
<td id="select"><input type="checkbox" name="${artist['ArtistID']}" class="checkbox" /></td>
|
||||||
<td class="column-albumart">
|
<td id="albumart"><div id="artistImg"><img class="albumArt" id="${artist['ArtistID']}" src="artwork/thumbs/artist/${artist['ArtistID']}" height="50" width="50"></div></td>
|
||||||
<div class="artistImg-container">
|
<td id="name"><span title="${artist['ArtistSortName']}"></span><a href="artistPage?ArtistID=${artist['ArtistID']}">${artist['ArtistName']}</a></td>
|
||||||
<%-- Using data-src for lazy loading with jquery.unveil.min.js and native loading="lazy" --%>
|
<td id="status">${artist['Status']}</td>
|
||||||
<img class="albumArt-thumb" alt="Album art for ${artist['ArtistName']}" data-src="artwork/thumbs/artist/${artist['ArtistID']}" loading="lazy" />
|
<td id="album"><span title="${releasedate}"></span><a href="albumPage?AlbumID=${artist['AlbumID']}">${albumdisplay}</a></td>
|
||||||
</div>
|
<td id="lastupdated">${lastupdated}</td>
|
||||||
</td>
|
|
||||||
<td class="column-name"><span title="${artist['ArtistSortName']}"></span><a href="artistPage?ArtistID=${artist['ArtistID']}">${artist['ArtistName']}</a></td>
|
|
||||||
<td class="column-status">${artist['Status']}</td>
|
|
||||||
<td class="column-album"><span title="${releasedate}"></span><a href="albumPage?AlbumID=${artist['AlbumID']}">${albumdisplay}</a></td>
|
|
||||||
<td class="column-lastupdated">${lastupdated}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
%endfor
|
%endfor
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -85,142 +80,37 @@
|
|||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="headIncludes()">
|
<%def name="headIncludes()">
|
||||||
${parent.headIncludes()} <%-- Ensure parent head includes are kept --%>
|
|
||||||
<link rel="stylesheet" href="interfaces/default/css/data_table.css">
|
<link rel="stylesheet" href="interfaces/default/css/data_table.css">
|
||||||
<style>
|
|
||||||
/* Basic CSS for album art thumbnail, consistent with index.html */
|
|
||||||
.albumArt-thumb {
|
|
||||||
height: 50px;
|
|
||||||
width: 50px;
|
|
||||||
object-fit: cover; /* Ensures image covers the area without distortion */
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Styles for artist status grades */
|
|
||||||
.gradeX { /* Paused */
|
|
||||||
background-color: #f2dede; /* Light red/pink */
|
|
||||||
}
|
|
||||||
.gradeC { /* Loading */
|
|
||||||
background-color: #dff0d8; /* Light green */
|
|
||||||
}
|
|
||||||
.gradeZ { /* Others (e.g., Active) */
|
|
||||||
/* No specific background or subtle light grey */
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="javascriptIncludes()">
|
<%def name="javascriptIncludes()">
|
||||||
${parent.javascriptIncludes()} <%-- Ensure parent javascript includes are kept --%>
|
|
||||||
<script src="js/libs/jquery.dataTables.min.js"></script>
|
<script src="js/libs/jquery.dataTables.min.js"></script>
|
||||||
<script src="js/libs/jquery.unveil.min.js"></script> <%-- Added for lazy loading --%>
|
|
||||||
<script>
|
<script>
|
||||||
// Encapsulate page-specific logic
|
function initThisPage() {
|
||||||
var ManageArtistsPage = ManageArtistsPage || {};
|
|
||||||
|
|
||||||
ManageArtistsPage.initDataTable = function() {
|
|
||||||
$('#artist_table').dataTable({
|
$('#artist_table').dataTable({
|
||||||
"bDestroy": true,
|
"bDestroy": true,
|
||||||
"aoColumns": [
|
"aoColumns": [
|
||||||
null, // Checkbox column (not sortable)
|
null,
|
||||||
null, // Album art (not sortable by default)
|
null,
|
||||||
{ "sType": "title-string"}, // Artist Name (uses title for sort)
|
{ "sType": "title-string"},
|
||||||
null, // Status
|
null,
|
||||||
{ "sType": "title-string"}, // Latest Album (uses title for sort)
|
{ "sType": "title-string"},
|
||||||
null // Last Updated
|
null
|
||||||
],
|
|
||||||
"aoColumnDefs": [
|
|
||||||
{ 'bSortable': false, 'aTargets': [ 0, 1 ] } // Disable sorting for checkbox and album art columns
|
|
||||||
],
|
],
|
||||||
"oLanguage": {
|
"oLanguage": {
|
||||||
"sSearch" : "",
|
"sSearch" : "",
|
||||||
"sEmptyTable": " "
|
"sEmptyTable": " "
|
||||||
},
|
},
|
||||||
"bStateSave": true, // Retain table state across page loads
|
"bStateSave": true,
|
||||||
"bPaginate": false, // All data loaded on one page
|
"bPaginate": false
|
||||||
"fnDrawCallback": function (o) {
|
|
||||||
// Re-unveil images after each draw for lazy loading
|
|
||||||
$("img.albumArt-thumb").unveil();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
ManageArtistsPage.initActions = function() {
|
|
||||||
// Event listener for the "Mark selected artists" dropdown
|
|
||||||
$('#markArtistActionSelect').on('change', function() {
|
|
||||||
var $this = $(this);
|
|
||||||
var actionValue = $this.val();
|
|
||||||
var selectedArtistIds = [];
|
|
||||||
|
|
||||||
// Get all checked artist checkboxes
|
|
||||||
$('.artist-checkbox:checked').each(function() {
|
|
||||||
selectedArtistIds.push($(this).val());
|
|
||||||
});
|
|
||||||
|
|
||||||
if (selectedArtistIds.length === 0) {
|
|
||||||
// Display error if no artists are selected
|
|
||||||
if (typeof showMessage === 'function') { // Assuming showMessage is a global utility
|
|
||||||
showMessage($this.data('error') || "You didn't select any artists", 'error');
|
|
||||||
} else {
|
|
||||||
alert($this.data('error') || "You didn't select any artists");
|
|
||||||
}
|
|
||||||
// Reset select box to default
|
|
||||||
$this.val($('option:first', $this).val());
|
|
||||||
return; // Stop execution
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct URL with selected artist IDs and action
|
|
||||||
var url = 'markArtists?action=' + encodeURIComponent(actionValue);
|
|
||||||
$.each(selectedArtistIds, function(index, id) {
|
|
||||||
url += '&artistid=' + encodeURIComponent(id); // Use a consistent param name if backend expects multiple
|
|
||||||
});
|
|
||||||
|
|
||||||
// Perform AJAX call
|
|
||||||
if (typeof doAjaxCall === 'function') {
|
|
||||||
doAjaxCall(url, $this, 'table', 'Artists marked as ' + actionValue + ' successfully!', 'Error marking artists.');
|
|
||||||
// After successful action, you might want to redraw the table or update rows
|
|
||||||
// $('#artist_table').dataTable().fnDraw();
|
|
||||||
} else {
|
|
||||||
console.error("doAjaxCall function is not defined. Cannot mark artists asynchronously.");
|
|
||||||
}
|
|
||||||
// Reset select box to default after action
|
|
||||||
$this.val($('option:first', $this).val());
|
|
||||||
});
|
|
||||||
|
|
||||||
// Event listener for the "toggle all" checkbox
|
|
||||||
$('#toggleAllArtists').on('click', function() {
|
|
||||||
$('.artist-checkbox').prop('checked', this.checked);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Optionally, update "toggle all" checkbox based on individual checkboxes
|
|
||||||
$(document).on('click', '.artist-checkbox', function() {
|
|
||||||
if (!this.checked) {
|
|
||||||
$('#toggleAllArtists').prop('checked', false);
|
|
||||||
} else {
|
|
||||||
// If all are checked, check the header checkbox
|
|
||||||
if ($('.artist-checkbox:checked').length === $('.artist-checkbox').length) {
|
|
||||||
$('#toggleAllArtists').prop('checked', true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$(document).ready(function() {
|
|
||||||
ManageArtistsPage.initDataTable();
|
|
||||||
ManageArtistsPage.initActions();
|
|
||||||
|
|
||||||
// Assuming resetFilters is a global function from common.js
|
|
||||||
if (typeof resetFilters === 'function') {
|
|
||||||
resetFilters("artists");
|
resetFilters("artists");
|
||||||
}
|
}
|
||||||
|
$(document).ready(function() {
|
||||||
|
initThisPage();
|
||||||
});
|
});
|
||||||
|
$(window).load(function(){
|
||||||
// Call initFancybox if it's a global function that needs to run on window load
|
|
||||||
$(window).on('load', function(){
|
|
||||||
if (typeof initFancybox === 'function') {
|
|
||||||
initFancybox();
|
initFancybox();
|
||||||
} else {
|
|
||||||
console.warn("initFancybox function is not defined.");
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
<%inherit file="base.html" />
|
<%inherit file="base.html" />
|
||||||
<%!
|
<%!
|
||||||
import headphones
|
import headphones
|
||||||
# Removed direct DB imports/interactions here, as data should be pre-fetched server-side.
|
from headphones import db, helpers
|
||||||
# from headphones import db, helpers
|
myDB = db.DBConnection()
|
||||||
# myDB = db.DBConnection() # This should not be in the template
|
|
||||||
%>
|
%>
|
||||||
|
|
||||||
<%def name="headerIncludes()">
|
<%def name="headerIncludes()">
|
||||||
@@ -21,82 +20,73 @@
|
|||||||
<h1 class="clearfix"><i class="fa fa-music"></i> Manage Manually Matched Albums</h1>
|
<h1 class="clearfix"><i class="fa fa-music"></i> Manage Manually Matched Albums</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="display" id="manual_album_table"> <%-- Changed ID for clarity --%>
|
<table class="display" id="artist_table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="column-artist">Local Artist</th> <%-- Changed ID to class --%>
|
<th id="artist">Local Artist</th>
|
||||||
<th class="column-album">Local Album</th> <%-- Changed ID to class --%>
|
<th id="album">Local Album</th>
|
||||||
<th class="column-status">Previous Action</th> <%-- Changed ID to class --%>
|
<th id="status">Previous Action</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<% count_albums=0 %> <%-- Keeping count_albums for potential unique element IDs if still absolutely necessary, though aiming for generic approach --%>
|
<% count_albums=0 %>
|
||||||
%for album in manualalbums:
|
%for album in manualalbums:
|
||||||
<tr class="gradeZ">
|
<tr class="gradeZ">
|
||||||
<%
|
<%
|
||||||
# These replacements should ideally be done server-side before passing to template,
|
|
||||||
# or in JS using encodeURIComponent, but kept here for minimal change.
|
|
||||||
old_artist_clean = album['ArtistName'].replace('&','%26').replace('+', '%2B').replace("'","%27")
|
old_artist_clean = album['ArtistName'].replace('&','%26').replace('+', '%2B').replace("'","%27")
|
||||||
old_album_clean = album['AlbumTitle'].replace('&','%26').replace('+', '%2B').replace("'","%27")
|
old_album_clean = album['AlbumTitle'].replace('&','%26').replace('+', '%2B').replace("'","%27")
|
||||||
%>
|
%>
|
||||||
<td class="column-artist">
|
<td id="artist">${album['ArtistName']}<BR>
|
||||||
${album['ArtistName']}<BR>
|
<button id="reset_artist${count_albums}" onClick="reset_Artist(this.id)">(<-) Reset Artist</button>
|
||||||
<%-- Use a common class and data attributes to pass info to generic dialog --%>
|
<div id="reset_artist_dialog${count_albums}" title="Reset Artist" style="display:none">
|
||||||
<button type="button" class="reset-button"
|
<table>
|
||||||
data-reset-type="artist"
|
<tr><td>Are you sure you want to reset Local Artist: ${album['ArtistName']} to unmatched?</td></tr>
|
||||||
data-artist-name="${album['ArtistName']}"
|
<tr><td align="right"><BR>
|
||||||
data-album-status="${album['AlbumStatus']}"
|
%if album['AlbumStatus'] == "Ignored":
|
||||||
data-old-artist-clean="${old_artist_clean}">
|
<button href="javascript:void(0)" onclick="doAjaxCall('markManual?action=unignoreArtist&existing_artist=${old_artist_clean}', $(this), 'page');" data-success="Successfully reset ${album['ArtistName']} to unmatched">Reset Artist</button>
|
||||||
(<-) Reset Artist
|
%elif album['AlbumStatus'] == "Matched":
|
||||||
</button>
|
<button href="javascript:void(0)" onclick="doAjaxCall('markManual?action=unmatchArtist&existing_artist=${old_artist_clean}', $(this), 'page');" data-success="Successfully restored ${album['ArtistName']} to unmatched">Reset Artist</button>
|
||||||
|
%endif
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="column-album">
|
<td id="album">${album['AlbumTitle']}<BR>
|
||||||
${album['AlbumTitle']}<BR>
|
<button id="reset_album${count_albums}" onClick="reset_Album(this.id)">(<-) Reset Album</button>
|
||||||
<%-- Use a common class and data attributes to pass info to generic dialog --%>
|
<div id="reset_album_dialog${count_albums}" title="Reset Album" style="display:none">
|
||||||
<button type="button" class="reset-button"
|
<table>
|
||||||
data-reset-type="album"
|
<tr><td>Are you sure you want to reset Local Album: ${album['AlbumTitle']} to unmatched?</td></tr>
|
||||||
data-artist-name="${album['ArtistName']}"
|
<tr><td align="right"><BR>
|
||||||
data-album-title="${album['AlbumTitle']}"
|
%if album['AlbumStatus'] == "Ignored":
|
||||||
data-album-status="${album['AlbumStatus']}"
|
<button href="javascript:void(0)" onclick="doAjaxCall('markManual?action=unignoreAlbum&existing_artist=${old_artist_clean}&existing_album=${old_album_clean}', $(this), 'page');" data-success="Successfully reset ${album['AlbumTitle']} to unmatched">Reset Album</button>
|
||||||
data-old-artist-clean="${old_artist_clean}"
|
%elif album['AlbumStatus'] == "Matched":
|
||||||
data-old-album-clean="${old_album_clean}">
|
<button href="javascript:void(0)" onclick="doAjaxCall('markManual?action=unmatchAlbum&existing_artist=${old_artist_clean}&existing_album=${old_album_clean}', $(this), 'page');" data-success="Successfully reset ${album['AlbumTitle']} to unmatched">Reset Album</button>
|
||||||
(<-) Reset Album
|
%endif
|
||||||
</button>
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
<td class="column-status">
|
<td id="status">${album['AlbumStatus']}
|
||||||
${album['AlbumStatus']}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<% count_albums+=1 %>
|
<% count_albums+=1 %>
|
||||||
%endfor
|
%endfor
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<%-- Generic Reset Confirmation Dialog (only one on the page) --%>
|
|
||||||
<div id="reset_dialog" title="Reset Confirmation" style="display:none">
|
|
||||||
<p class="dialog-message"></p>
|
|
||||||
<p class="dialog-actions" align="right"><BR>
|
|
||||||
<button type="button" class="confirm-reset-button"></button>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="headIncludes()">
|
<%def name="headIncludes()">
|
||||||
${parent.headIncludes()} <%-- Ensure parent head includes are kept --%>
|
|
||||||
<link rel="stylesheet" href="interfaces/default/css/data_table.css">
|
<link rel="stylesheet" href="interfaces/default/css/data_table.css">
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="javascriptIncludes()">
|
<%def name="javascriptIncludes()">
|
||||||
${parent.javascriptIncludes()} <%-- Ensure parent javascript includes are kept --%>
|
|
||||||
<script src="js/libs/jquery.dataTables.min.js"></script>
|
<script src="js/libs/jquery.dataTables.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Encapsulate page-specific logic
|
$(document).ready(function() {
|
||||||
var ManageManualPage = ManageManualPage || {};
|
$('#artist_table').dataTable({
|
||||||
|
|
||||||
ManageManualPage.initDataTable = function() {
|
|
||||||
$('#manual_album_table').dataTable({ <%-- Use the updated ID --%>
|
|
||||||
"bStateSave": true,
|
"bStateSave": true,
|
||||||
"bPaginate": true,
|
"bPaginate": true,
|
||||||
"oLanguage": {
|
"oLanguage": {
|
||||||
@@ -113,91 +103,21 @@
|
|||||||
$('html,body').scrollTop(0);
|
$('html,body').scrollTop(0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
ManageManualPage.initDialogs = function() {
|
|
||||||
// Initialize the generic reset dialog once
|
|
||||||
$('#reset_dialog').dialog({
|
|
||||||
autoOpen: false,
|
|
||||||
modal: true,
|
|
||||||
resizable: false,
|
|
||||||
width: 500,
|
|
||||||
height: 'auto',
|
|
||||||
buttons: {
|
|
||||||
"Cancel": function() {
|
|
||||||
$(this).dialog('close');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
ManageManualPage.initActions = function() {
|
|
||||||
// Event delegation for all reset buttons
|
|
||||||
$(document).on('click', '.reset-button', function() {
|
|
||||||
var $button = $(this);
|
|
||||||
var resetType = $button.data('reset-type'); // 'artist' or 'album'
|
|
||||||
var artistName = $button.data('artist-name');
|
|
||||||
var albumTitle = $button.data('album-title');
|
|
||||||
var albumStatus = $button.data('album-status');
|
|
||||||
var oldArtistClean = $button.data('old-artist-clean');
|
|
||||||
var oldAlbumClean = $button.data('old-album-clean');
|
|
||||||
|
|
||||||
var $dialog = $('#reset_dialog');
|
|
||||||
var $dialogMessage = $dialog.find('.dialog-message');
|
|
||||||
var $confirmButton = $dialog.find('.confirm-reset-button');
|
|
||||||
|
|
||||||
var message = "";
|
|
||||||
var actionUrl = "";
|
|
||||||
var successMessage = "";
|
|
||||||
|
|
||||||
if (resetType === 'artist') {
|
|
||||||
message = "Are you sure you want to reset Local Artist: " + artistName + " to unmatched?";
|
|
||||||
if (albumStatus === "Ignored") {
|
|
||||||
actionUrl = 'markManual?action=unignoreArtist&existing_artist=' + oldArtistClean;
|
|
||||||
successMessage = "Successfully reset " + artistName + " to unmatched";
|
|
||||||
} else if (albumStatus === "Matched") {
|
|
||||||
actionUrl = 'markManual?action=unmatchArtist&existing_artist=' + oldArtistClean;
|
|
||||||
successMessage = "Successfully restored " + artistName + " to unmatched";
|
|
||||||
}
|
|
||||||
$confirmButton.text('Reset Artist');
|
|
||||||
} else if (resetType === 'album') {
|
|
||||||
message = "Are you sure you want to reset Local Album: " + albumTitle + " to unmatched?";
|
|
||||||
if (albumStatus === "Ignored") {
|
|
||||||
actionUrl = 'markManual?action=unignoreAlbum&existing_artist=' + oldArtistClean + '&existing_album=' + oldAlbumClean;
|
|
||||||
successMessage = "Successfully reset " + albumTitle + " to unmatched";
|
|
||||||
} else if (albumStatus === "Matched") {
|
|
||||||
actionUrl = 'markManual?action=unmatchAlbum&existing_artist=' + oldArtistClean + '&existing_album=' + oldAlbumClean;
|
|
||||||
successMessage = "Successfully reset " + albumTitle + " to unmatched";
|
|
||||||
}
|
|
||||||
$confirmButton.text('Reset Album');
|
|
||||||
}
|
|
||||||
|
|
||||||
$dialogMessage.text(message);
|
|
||||||
|
|
||||||
// Unbind previous click handler and bind new one
|
|
||||||
$confirmButton.off('click').on('click', function() {
|
|
||||||
if (typeof doAjaxCall === 'function') {
|
|
||||||
doAjaxCall(actionUrl, $button, 'page', successMessage);
|
|
||||||
} else {
|
|
||||||
console.error("doAjaxCall function is not defined. Cannot perform reset action.");
|
|
||||||
}
|
|
||||||
$dialog.dialog('close');
|
|
||||||
});
|
|
||||||
|
|
||||||
$dialog.dialog('open');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assuming initActions from common.js is called globally
|
|
||||||
if (typeof initActions === 'function') {
|
|
||||||
initActions();
|
initActions();
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$(document).ready(function() {
|
|
||||||
ManageManualPage.initDataTable();
|
|
||||||
ManageManualPage.initDialogs();
|
|
||||||
ManageManualPage.initActions();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function reset_Artist(clicked_id) {
|
||||||
|
n=clicked_id.replace("reset_artist","");
|
||||||
|
$("#reset_artist_dialog"+n).dialog();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset_Album(clicked_id) {
|
||||||
|
n=clicked_id.replace("reset_album","");
|
||||||
|
$("#reset_album_dialog"+n).dialog();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
<%def name="headerIncludes()">
|
<%def name="headerIncludes()">
|
||||||
<div id="subhead_container">
|
<div id="subhead_container">
|
||||||
<div id="subhead_menu">
|
<div id="subhead_menu">
|
||||||
<%-- Changed inline onclick to a class and data attributes for JS handling --%>
|
<a id="menu_link_scan" onclick="doAjaxCall('musicScan?path=${headphones.CONFIG.MUSIC_DIR}&redirect=manageNew',$(this))" data-success="Music library is getting scanned">Scan Music Library</a>
|
||||||
<a id="menu_link_scan" class="ajax-link" data-action="musicScan" data-path="${headphones.CONFIG.MUSIC_DIR}" data-redirect="manageNew" data-success="Music library is getting scanned" href="#">Scan Music Library</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a href="manage" class="back">« Back to manage overview</a>
|
<a href="manage" class="back">« Back to manage overview</a>
|
||||||
@@ -19,28 +18,26 @@
|
|||||||
<div id="manageheader" class="title">
|
<div id="manageheader" class="title">
|
||||||
<h1 class="clearfix"><i class="fa fa-music"></i> Manage New Artists</h1>
|
<h1 class="clearfix"><i class="fa fa-music"></i> Manage New Artists</h1>
|
||||||
</div>
|
</div>
|
||||||
<form action="addArtists" method="get" id="addArtistsForm"> <%-- Added ID to the form --%>
|
<form action="addArtists" method="get">
|
||||||
<div id="new_artists_controls"> <%-- Unique and descriptive ID --%>
|
<div id="markalbum">
|
||||||
<select name="action" id="newArtistActionSelect">
|
<select name="action">
|
||||||
<option value="add">(+) ADD Selected Artists</option>
|
<option value="add">(+) ADD Selected Artists</option>
|
||||||
<option value="ignore">(-) IGNORE Selected Artists</option>
|
<option value="ignore">(-) IGNORE Selected Artists</option>
|
||||||
</select>
|
</select>
|
||||||
<%-- Changed input type="submit" to button with class for AJAX handling --%>
|
<input type="submit" value="Go">
|
||||||
<button type="button" id="submitNewArtistAction" class="ajax-submit-button" data-form-id="addArtistsForm" data-action="addArtists" data-error="You didn't select any artists">Go</button>
|
|
||||||
</div>
|
</div>
|
||||||
<table class="display" id="artist_table">
|
<table class="display" id="artist_table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="select-all-checkbox"><input type="checkbox" id="toggleAllNewArtists" /></th> <%-- Added ID for easier targeting --%>
|
<th id="select"><input type="checkbox" onClick="toggle(this)" /></th>
|
||||||
<th class="column-name">Artist Name</th> <%-- Changed ID to class --%>
|
<th id="name">Artist Name</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
%for artist in newartists:
|
%for artist in newartists:
|
||||||
<tr class="gradeZ">
|
<tr class="gradeZ">
|
||||||
<%-- Changed name attribute to a consistent 'artist_ids' and value to ArtistID for robust processing --%>
|
<td id="select"><input type="checkbox" name="${artist['ArtistName']}" class="checkbox" /></td>
|
||||||
<td class="select-artist-checkbox"><input type="checkbox" name="artist_ids" value="${artist['ArtistID']}" class="new-artist-checkbox" /></td>
|
<td id="name">${artist['ArtistName']}</a></td>
|
||||||
<td class="column-name">${artist['ArtistName']}</a></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
%endfor
|
%endfor
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -50,111 +47,22 @@
|
|||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="headIncludes()">
|
<%def name="headIncludes()">
|
||||||
${parent.headIncludes()} <%-- Ensure parent head includes are kept --%>
|
|
||||||
<link rel="stylesheet" href="interfaces/default/css/data_table.css">
|
<link rel="stylesheet" href="interfaces/default/css/data_table.css">
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="javascriptIncludes()">
|
<%def name="javascriptIncludes()">
|
||||||
${parent.javascriptIncludes()} <%-- Ensure parent javascript includes are kept --%>
|
|
||||||
<script src="js/libs/jquery.dataTables.min.js"></script>
|
<script src="js/libs/jquery.dataTables.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Encapsulate page-specific logic
|
$(document).ready(function() {
|
||||||
var ManageNewArtistsPage = ManageNewArtistsPage || {};
|
|
||||||
|
|
||||||
ManageNewArtistsPage.initDataTable = function() {
|
|
||||||
$('#artist_table').dataTable({
|
$('#artist_table').dataTable({
|
||||||
"aaSorting": [[1, 'asc']], // Sort by Artist Name ascending
|
"aaSorting": [[1, 'asc']],
|
||||||
"bStateSave": false,
|
"bStateSave": false,
|
||||||
"bPaginate": false,
|
"bPaginate": false,
|
||||||
"oLanguage": {
|
"oLanguage": {
|
||||||
"sSearch" : "",
|
"sSearch" : ""},
|
||||||
"sEmptyTable": "No new artist information available"
|
|
||||||
},
|
|
||||||
"fnDrawCallback": function (o) {
|
|
||||||
$('html,body').scrollTop(0); // Jump to top of page on draw
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
ManageNewArtistsPage.initActions = function() {
|
|
||||||
// Event listener for the "Scan Music Library" link in the header
|
|
||||||
$(document).on('click', '#menu_link_scan', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
var $this = $(this);
|
|
||||||
var path = $this.data('path');
|
|
||||||
var redirect = $this.data('redirect');
|
|
||||||
var successMsg = $this.data('success');
|
|
||||||
var url = $this.data('action') + '?path=' + encodeURIComponent(path) + '&redirect=' + encodeURIComponent(redirect);
|
|
||||||
|
|
||||||
if (typeof doAjaxCall === 'function') {
|
|
||||||
doAjaxCall(url, $this, null, successMsg); // 'null' for context if not affecting a specific tab/table
|
|
||||||
} else {
|
|
||||||
console.error("doAjaxCall function is not defined. Cannot scan music library.");
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Event listener for the "Go" button to add/ignore artists
|
|
||||||
$('#submitNewArtistAction').on('click', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
var $this = $(this);
|
|
||||||
var action = $('#newArtistActionSelect').val();
|
|
||||||
var selectedArtistIds = [];
|
|
||||||
|
|
||||||
// Get all checked new artist checkboxes
|
|
||||||
$('.new-artist-checkbox:checked').each(function() {
|
|
||||||
selectedArtistIds.push($(this).val());
|
|
||||||
});
|
|
||||||
|
|
||||||
if (selectedArtistIds.length === 0) {
|
|
||||||
if (typeof showMessage === 'function') {
|
|
||||||
showMessage($this.data('error') || "You didn't select any artists", 'error');
|
|
||||||
} else {
|
|
||||||
alert($this.data('error') || "You didn't select any artists");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var url = $this.data('action') + '?action=' + encodeURIComponent(action);
|
|
||||||
$.each(selectedArtistIds, function(index, id) {
|
|
||||||
url += '&artist_ids=' + encodeURIComponent(id); // Append selected artist IDs
|
|
||||||
});
|
|
||||||
|
|
||||||
var successMsg = 'Artists successfully ' + (action === 'add' ? 'added' : 'ignored');
|
|
||||||
var errorMsg = 'Error performing action on artists.';
|
|
||||||
|
|
||||||
if (typeof doAjaxCall === 'function') {
|
|
||||||
doAjaxCall(url, $this, 'table', successMsg, errorMsg);
|
|
||||||
// Optionally, refresh the table or remove processed rows after successful AJAX call
|
|
||||||
} else {
|
|
||||||
console.error("doAjaxCall function is not defined. Cannot add/ignore artists.");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Event listener for the "toggle all" checkbox
|
|
||||||
$('#toggleAllNewArtists').on('click', function() {
|
|
||||||
$('.new-artist-checkbox').prop('checked', this.checked);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update "toggle all" checkbox based on individual checkboxes
|
|
||||||
$(document).on('click', '.new-artist-checkbox', function() {
|
|
||||||
if (!this.checked) {
|
|
||||||
$('#toggleAllNewArtists').prop('checked', false);
|
|
||||||
} else {
|
|
||||||
if ($('.new-artist-checkbox:checked').length === $('.new-artist-checkbox').length) {
|
|
||||||
$('#toggleAllNewArtists').prop('checked', true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assuming initActions from common.js is called globally
|
|
||||||
if (typeof initActions === 'function') {
|
|
||||||
initActions();
|
initActions();
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$(document).ready(function() {
|
|
||||||
ManageNewArtistsPage.initDataTable();
|
|
||||||
ManageNewArtistsPage.initActions();
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|||||||
@@ -1,20 +1,16 @@
|
|||||||
<%inherit file="base.html" />
|
<%inherit file="base.html" />
|
||||||
<%!
|
<%!
|
||||||
import headphones
|
import headphones
|
||||||
# Removed direct DB imports and queries from template.
|
import json
|
||||||
# These operations (fetching artists and creating json_artists)
|
from headphones import db, helpers
|
||||||
# should be performed in the Python view/controller and passed
|
myDB = db.DBConnection()
|
||||||
# to the template as part of its context.
|
artist_json = {}
|
||||||
# import json
|
counter = 0
|
||||||
# from headphones import db, helpers
|
artist_list = myDB.action("SELECT ArtistName from artists ORDER BY ArtistName COLLATE NOCASE")
|
||||||
# myDB = db.DBConnection()
|
for artist in artist_list:
|
||||||
# artist_json = {}
|
artist_json[counter] = artist['ArtistName']
|
||||||
# counter = 0
|
counter+=1
|
||||||
# artist_list = myDB.action("SELECT ArtistName from artists ORDER BY ArtistName COLLATE NOCASE")
|
json_artists = json.dumps(artist_json)
|
||||||
# for artist in artist_list:
|
|
||||||
# artist_json[counter] = artist['ArtistName']
|
|
||||||
# counter+=1
|
|
||||||
# json_artists = json.dumps(artist_json)
|
|
||||||
%>
|
%>
|
||||||
|
|
||||||
<%def name="headerIncludes()">
|
<%def name="headerIncludes()">
|
||||||
@@ -33,109 +29,97 @@
|
|||||||
<h1 class="clearfix"><i class="fa fa-music"></i> Manage Unmatched Albums</h1>
|
<h1 class="clearfix"><i class="fa fa-music"></i> Manage Unmatched Albums</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="display" id="unmatched_album_table"> <%-- Changed ID for clarity --%>
|
<table class="display" id="artist_table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="column-artist">Local Artist</th>
|
<th id="artist">Local Artist</th>
|
||||||
<th class="column-album">Local Album</th>
|
<th id="album">Local Album</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<% count_albums=0 %> <%-- Still useful for unique IDs if needed, but aiming for generic dialogs --%>
|
<% count_albums=0 %>
|
||||||
%for album in unmatchedalbums:
|
%for album in unmatchedalbums:
|
||||||
<tr class="gradeZ">
|
<tr class="gradeZ">
|
||||||
<%
|
<%
|
||||||
# Pre-escape for direct use in data-attributes where JS string is needed.
|
old_artist_clean = album['ArtistName'].replace('&','%26').replace("'","%27")
|
||||||
# URL encoding will happen in JS with encodeURIComponent.
|
old_album_clean = album['AlbumTitle'].replace('&','%26').replace("'","%27")
|
||||||
old_artist_js_str = album['ArtistName'].replace("'","\\'").replace('"','"')
|
old_artist_js = album['ArtistName'].replace("'","\\'").replace('"','\\"')
|
||||||
old_album_js_str = album['AlbumTitle'].replace("'","\\'").replace('"','"')
|
old_album_js = album['AlbumTitle'].replace("'","\\'").replace('"','\\"')
|
||||||
%>
|
%>
|
||||||
<td class="column-artist">
|
<td id="artist">${album['ArtistName']}<BR>
|
||||||
${album['ArtistName']}<BR>
|
<button id="ignore_artists${count_albums}" onClick="ignore_Artist(this.id)">(-) Ignore Artist</button>
|
||||||
<%-- Data attributes to pass context to JS --%>
|
<div id="ignore_artist_dialog${count_albums}" title="Ignore Artist" style="display:none">
|
||||||
<button type="button" class="action-button ignore-artist-button"
|
<table>
|
||||||
data-artist-name="${album['ArtistName']}"
|
<tr><td>Are you sure you want to ignore Local Artist: ${album['ArtistName']} from future matching?</td></tr>
|
||||||
data-old-artist-js="${old_artist_js_str}">
|
<tr><td align="right"><BR>
|
||||||
(-) Ignore Artist
|
<button href="javascript:void(0)" onclick="doAjaxCall('markUnmatched?action=ignoreArtist&existing_artist=${old_artist_clean}', $(this), 'page');" data-success="Successfully ignored ${album['ArtistName']} from future matching">Ignore Artist</button>
|
||||||
</button>
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="match_artists${count_albums}" onClick="load_Artist(this.id)">(->) Match Artist</button>
|
||||||
|
<div id="artist_dialog${count_albums}" title="Match Artist" style="display:none">
|
||||||
|
<table width=400>
|
||||||
|
<tr><td>Local Artist:</td><td>${album['ArtistName']}</td></tr>
|
||||||
|
<tr><td>Match Artist</td><td>
|
||||||
|
<select id="artist_options${count_albums}" name="new_artist">
|
||||||
|
</select>
|
||||||
|
</td></tr>
|
||||||
|
<tr><td></td><td align="right"><BR>
|
||||||
|
<button href="javascript:void(0)" onclick="artist_matcher(${count_albums}, '${old_artist_js}')">Match Artist</button>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="button" class="action-button match-artist-button"
|
|
||||||
data-artist-name="${album['ArtistName']}"
|
|
||||||
data-old-artist-js="${old_artist_js_str}">
|
|
||||||
(->) Match Artist
|
|
||||||
</button>
|
|
||||||
</td>
|
</td>
|
||||||
<td class="column-album">
|
<td id="album">${album['AlbumTitle']}<BR>
|
||||||
${album['AlbumTitle']}<BR>
|
<button id="ignore_albums${count_albums}" onClick="ignore_Album(this.id)">(-) Ignore Album</button>
|
||||||
<button type="button" class="action-button ignore-album-button"
|
<div id="ignore_album_dialog${count_albums}" title="Ignore Album" style="display:none">
|
||||||
data-artist-name="${album['ArtistName']}"
|
<table>
|
||||||
data-album-title="${album['AlbumTitle']}"
|
<tr><td>Are you sure you want to ignore Local Album: ${album['AlbumTitle']} from future matching?</td></tr>
|
||||||
data-old-artist-js="${old_artist_js_str}"
|
<tr><td align="right"><BR>
|
||||||
data-old-album-js="${old_album_js_str}">
|
<button href="javascript:void(0)" onclick="doAjaxCall('markUnmatched?action=ignoreAlbum&existing_artist=${old_artist_clean}&existing_album=${old_album_clean}', $(this), 'page');" data-success="Successfully ignored ${album['AlbumTitle']} from future matching">Ignore Album</button>
|
||||||
(-) Ignore Album
|
</td></tr>
|
||||||
</button>
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="match_albums${count_albums}" onClick="load_AlbumArtist(this.id)">(->) Match Album</button>
|
||||||
|
<div id="album_dialog${count_albums}" title="Match Album" style="display:none">
|
||||||
|
<table width=650>
|
||||||
|
<tr><td>Local Artist:</td><td>${album['ArtistName']}</td></tr>
|
||||||
|
<tr><td>Local Album:</td><td>${album['AlbumTitle']}</td></tr>
|
||||||
|
<tr><td>Match Artist</td><td>
|
||||||
|
<select id="album_artist_options${count_albums}" name="new_artist">
|
||||||
|
</select>
|
||||||
|
</td></tr>
|
||||||
|
<tr><td>Match Album</td><td>
|
||||||
|
<select id="album_options${count_albums}" name="new_album">
|
||||||
|
</select>
|
||||||
|
</td></tr>
|
||||||
|
<tr><td></td><td align="right"><BR>
|
||||||
|
<button href="javascript:void(0)" onclick="album_matcher(${count_albums}, '${old_artist_js}', '${old_album_js}')">Match Album</button>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="button" class="action-button match-album-button"
|
|
||||||
data-artist-name="${album['ArtistName']}"
|
|
||||||
data-album-title="${album['AlbumTitle']}"
|
|
||||||
data-old-artist-js="${old_artist_js_str}"
|
|
||||||
data-old-album-js="${old_album_js_str}">
|
|
||||||
(->) Match Album
|
|
||||||
</button>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<% count_albums+=1 %>
|
<% count_albums+=1 %>
|
||||||
%endfor
|
%endfor
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<%-- Generic Ignore Confirmation Dialog --%>
|
|
||||||
<div id="ignore_dialog" title="Confirm Ignore" style="display:none">
|
|
||||||
<p class="dialog-message"></p>
|
|
||||||
<p class="dialog-actions" align="right"><BR>
|
|
||||||
<button type="button" class="confirm-ignore-button"></button>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<%-- Generic Match Dialog --%>
|
|
||||||
<div id="match_dialog" title="Match Album" style="display:none">
|
|
||||||
<p><strong>Local Artist:</strong> <span id="local-artist-display"></span></p>
|
|
||||||
<p><strong>Local Album:</strong> <span id="local-album-display"></span></p>
|
|
||||||
|
|
||||||
<div id="artist-match-section">
|
|
||||||
<p>Match Artist:</p>
|
|
||||||
<select id="match-artist-options" name="new_artist"></select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="album-match-section">
|
|
||||||
<p>Match Album:</p>
|
|
||||||
<select id="match-album-options" name="new_album"></select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="dialog-actions" align="right"><BR>
|
|
||||||
<button type="button" class="confirm-match-button"></button>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="headIncludes()">
|
<%def name="headIncludes()">
|
||||||
${parent.headIncludes()} <%-- Ensure parent head includes are kept --%>
|
|
||||||
<link rel="stylesheet" href="interfaces/default/css/data_table.css">
|
<link rel="stylesheet" href="interfaces/default/css/data_table.css">
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="javascriptIncludes()">
|
<%def name="javascriptIncludes()">
|
||||||
${parent.javascriptIncludes()} <%-- Ensure parent javascript includes are kept --%>
|
|
||||||
<script src="js/libs/jquery.dataTables.min.js"></script>
|
<script src="js/libs/jquery.dataTables.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Encapsulate page-specific logic
|
$(document).ready(function() {
|
||||||
var ManageUnmatchedPage = ManageUnmatchedPage || {};
|
$('#artist_table').dataTable({
|
||||||
|
|
||||||
ManageUnmatchedPage.jsonArtists = ${json_artists | n, unicode}; // Assuming json_artists is passed from backend
|
|
||||||
|
|
||||||
ManageUnmatchedPage.initDataTable = function() {
|
|
||||||
$('#unmatched_album_table').dataTable({ <%-- Use the updated ID --%>
|
|
||||||
"bStateSave": true,
|
"bStateSave": true,
|
||||||
"bPaginate": true,
|
"bPaginate": true,
|
||||||
"oLanguage": {
|
"oLanguage": {
|
||||||
@@ -152,196 +136,126 @@
|
|||||||
$('html,body').scrollTop(0);
|
$('html,body').scrollTop(0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
ManageUnmatchedPage.initDialogs = function() {
|
|
||||||
// Initialize the generic ignore dialog
|
|
||||||
$('#ignore_dialog').dialog({
|
|
||||||
autoOpen: false,
|
|
||||||
modal: true,
|
|
||||||
resizable: false,
|
|
||||||
width: 500,
|
|
||||||
height: 'auto',
|
|
||||||
buttons: {
|
|
||||||
"Cancel": function() {
|
|
||||||
$(this).dialog('close');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize the generic match dialog
|
|
||||||
$('#match_dialog').dialog({
|
|
||||||
autoOpen: false,
|
|
||||||
modal: true,
|
|
||||||
resizable: false,
|
|
||||||
width: 700,
|
|
||||||
height: 'auto',
|
|
||||||
buttons: {
|
|
||||||
"Cancel": function() {
|
|
||||||
$(this).dialog('close');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
ManageUnmatchedPage.initActions = function() {
|
|
||||||
// --- Ignore Artist/Album Logic ---
|
|
||||||
$(document).on('click', '.ignore-artist-button, .ignore-album-button', function() {
|
|
||||||
var $button = $(this);
|
|
||||||
var isArtistIgnore = $button.hasClass('ignore-artist-button');
|
|
||||||
var artistName = $button.data('artist-name');
|
|
||||||
var albumTitle = $button.data('album-title');
|
|
||||||
var oldArtistJs = $button.data('old-artist-js'); // JS escaped, needs URI encoding for URL
|
|
||||||
var oldAlbumJs = $button.data('old-album-js'); // JS escaped, needs URI encoding for URL
|
|
||||||
|
|
||||||
var $dialog = $('#ignore_dialog');
|
|
||||||
var $dialogMessage = $dialog.find('.dialog-message');
|
|
||||||
var $confirmButton = $dialog.find('.confirm-ignore-button');
|
|
||||||
|
|
||||||
var message = "";
|
|
||||||
var actionUrl = "";
|
|
||||||
var successMessage = "";
|
|
||||||
|
|
||||||
if (isArtistIgnore) {
|
|
||||||
message = "Are you sure you want to ignore Local Artist: " + artistName + " from future matching?";
|
|
||||||
actionUrl = 'markUnmatched?action=ignoreArtist&existing_artist=' + encodeURIComponent(oldArtistJs);
|
|
||||||
successMessage = "Successfully ignored " + artistName + " from future matching";
|
|
||||||
$confirmButton.text('Ignore Artist');
|
|
||||||
} else { // ignore-album-button
|
|
||||||
message = "Are you sure you want to ignore Local Album: " + albumTitle + " from future matching?";
|
|
||||||
actionUrl = 'markUnmatched?action=ignoreAlbum&existing_artist=' + encodeURIComponent(oldArtistJs) + '&existing_album=' + encodeURIComponent(oldAlbumJs);
|
|
||||||
successMessage = "Successfully ignored " + albumTitle + " from future matching";
|
|
||||||
$confirmButton.text('Ignore Album');
|
|
||||||
}
|
|
||||||
|
|
||||||
$dialogMessage.text(message);
|
|
||||||
|
|
||||||
$confirmButton.off('click').on('click', function() {
|
|
||||||
if (typeof doAjaxCall === 'function') {
|
|
||||||
doAjaxCall(actionUrl, $button, 'page', successMessage);
|
|
||||||
} else {
|
|
||||||
console.error("doAjaxCall function is not defined. Cannot perform ignore action.");
|
|
||||||
}
|
|
||||||
$dialog.dialog('close');
|
|
||||||
});
|
|
||||||
|
|
||||||
$dialog.dialog('open');
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- Match Artist/Album Logic ---
|
|
||||||
$(document).on('click', '.match-artist-button, .match-album-button', function() {
|
|
||||||
var $button = $(this);
|
|
||||||
var isArtistMatch = $button.hasClass('match-artist-button');
|
|
||||||
var artistName = $button.data('artist-name');
|
|
||||||
var albumTitle = $button.data('album-title');
|
|
||||||
var oldArtistJs = $button.data('old-artist-js');
|
|
||||||
var oldAlbumJs = $button.data('old-album-js');
|
|
||||||
|
|
||||||
var $dialog = $('#match_dialog');
|
|
||||||
var $localArtistDisplay = $('#local-artist-display');
|
|
||||||
var $localAlbumDisplay = $('#local-album-display');
|
|
||||||
var $artistMatchSection = $('#artist-match-section');
|
|
||||||
var $albumMatchSection = $('#album-match-section');
|
|
||||||
var $matchArtistOptions = $('#match-artist-options');
|
|
||||||
var $matchAlbumOptions = $('#match-album-options');
|
|
||||||
var $confirmButton = $dialog.find('.confirm-match-button');
|
|
||||||
|
|
||||||
// Reset dialog state
|
|
||||||
$localArtistDisplay.text(artistName);
|
|
||||||
$matchArtistOptions.empty();
|
|
||||||
$matchAlbumOptions.empty();
|
|
||||||
$confirmButton.off('click'); // Clear previous click handlers
|
|
||||||
|
|
||||||
// Populate artist options
|
|
||||||
$.each(ManageUnmatchedPage.jsonArtists, function(key, value) {
|
|
||||||
$matchArtistOptions.append($("<option/>", {
|
|
||||||
value: value,
|
|
||||||
text: value
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isArtistMatch) {
|
|
||||||
$localAlbumDisplay.closest('p').hide(); // Hide local album row
|
|
||||||
$albumMatchSection.hide(); // Hide match album section
|
|
||||||
$localArtistDisplay.text(artistName);
|
|
||||||
$artistMatchSection.show(); // Show match artist section
|
|
||||||
$confirmButton.text('Match Artist');
|
|
||||||
|
|
||||||
$confirmButton.on('click', function() {
|
|
||||||
var newArtist = $matchArtistOptions.val();
|
|
||||||
var actionUrl = 'markUnmatched?action=matchArtist&existing_artist=' + encodeURIComponent(oldArtistJs) + '&new_artist=' + encodeURIComponent(newArtist);
|
|
||||||
var successMessage = 'Successfully matched ' + artistName + ' with ' + newArtist;
|
|
||||||
if (typeof doAjaxCall === 'function') {
|
|
||||||
doAjaxCall(actionUrl, $button, 'page', successMessage);
|
|
||||||
} else {
|
|
||||||
console.error("doAjaxCall function is not defined. Cannot match artist.");
|
|
||||||
}
|
|
||||||
$dialog.dialog('close');
|
|
||||||
});
|
|
||||||
|
|
||||||
} else { // match-album-button
|
|
||||||
$localAlbumDisplay.closest('p').show(); // Show local album row
|
|
||||||
$albumMatchSection.show(); // Show match album section
|
|
||||||
$localAlbumDisplay.text(albumTitle);
|
|
||||||
$artistMatchSection.show(); // Show match artist section (for selecting matched album's artist)
|
|
||||||
$confirmButton.text('Match Album');
|
|
||||||
|
|
||||||
// Load albums for selected artist initially
|
|
||||||
ManageUnmatchedPage.loadAlbumsForArtist($matchArtistOptions.val(), $matchAlbumOptions);
|
|
||||||
|
|
||||||
// Handle artist selection change
|
|
||||||
$matchArtistOptions.off('change').on('change', function() {
|
|
||||||
ManageUnmatchedPage.loadAlbumsForArtist($(this).val(), $matchAlbumOptions);
|
|
||||||
});
|
|
||||||
|
|
||||||
$confirmButton.on('click', function() {
|
|
||||||
var newArtist = $matchArtistOptions.val();
|
|
||||||
var newAlbum = $matchAlbumOptions.val();
|
|
||||||
var actionUrl = 'markUnmatched?action=matchAlbum&existing_artist=' + encodeURIComponent(oldArtistJs) + '&new_artist=' + encodeURIComponent(newArtist) + '&existing_album=' + encodeURIComponent(oldAlbumJs) + '&new_album=' + encodeURIComponent(newAlbum);
|
|
||||||
var successMessage = 'Successfully matched ' + albumTitle + ' with ' + newAlbum + ' by ' + newArtist;
|
|
||||||
if (typeof doAjaxCall === 'function') {
|
|
||||||
doAjaxCall(actionUrl, $button, 'page', successMessage);
|
|
||||||
} else {
|
|
||||||
console.error("doAjaxCall function is not defined. Cannot match album.");
|
|
||||||
}
|
|
||||||
$dialog.dialog('close');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
$dialog.dialog('open');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Helper function to load albums for a given artist
|
|
||||||
ManageUnmatchedPage.loadAlbumsForArtist = function(artistName, $targetSelect) {
|
|
||||||
$targetSelect.empty().append($("<option/>", { value: "", text: "Loading albums..." })); // Add loading message
|
|
||||||
var cleanArtistName = encodeURIComponent(artistName);
|
|
||||||
$.getJSON("getAlbumsByArtist_json?artist=" + cleanArtistName, function(data) {
|
|
||||||
$targetSelect.empty();
|
|
||||||
if (Object.keys(data).length > 0) {
|
|
||||||
$.each(data, function(key, value) {
|
|
||||||
$targetSelect.append($("<option/>", {
|
|
||||||
value: value,
|
|
||||||
text: value
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
$targetSelect.append($("<option/>", { value: "", text: "No albums found" }));
|
|
||||||
}
|
|
||||||
}).fail(function() {
|
|
||||||
$targetSelect.empty().append($("<option/>", { value: "", text: "Error loading albums" }));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Assuming initActions from common.js is called globally
|
|
||||||
if (typeof initActions === 'function') {
|
|
||||||
initActions();
|
initActions();
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$(document).ready(function() {
|
|
||||||
ManageUnmatchedPage.initDataTable();
|
|
||||||
ManageUnmatchedPage.initDialogs();
|
|
||||||
ManageUnmatchedPage.initActions();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function ignore_Artist(clicked_id) {
|
||||||
|
n=clicked_id.replace("ignore_artists","");
|
||||||
|
$("#ignore_artist_dialog"+n).dialog();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ignore_Album(clicked_id) {
|
||||||
|
n=clicked_id.replace("ignore_albums","");
|
||||||
|
$("#ignore_album_dialog"+n).dialog();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function load_Artist(clicked_id) {
|
||||||
|
n=clicked_id.replace("match_artists","");
|
||||||
|
var d = $("#artist_dialog"+n).dialog();
|
||||||
|
d.dialog("option", "width", 450);
|
||||||
|
d.dialog("option", "position", "center");
|
||||||
|
$('#artist_options'+n).html('');
|
||||||
|
$.each(${json_artists}, function(key, value) {
|
||||||
|
$('#artist_options'+n).append($("<option/>", {
|
||||||
|
value: value,
|
||||||
|
text: value
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
change_artist(n)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function change_artist(n) {
|
||||||
|
selected_artist = $("#artist_options"+n).find("option:selected").text();
|
||||||
|
selected_artist_clean = selected_artist.replace('&', '%26').replace('+', '%2B');
|
||||||
|
$("#artist_options"+n).change(function() {
|
||||||
|
selected_artist = $("#artist_options"+n).find("option:selected").text();
|
||||||
|
selected_artist_clean = selected_artist.replace('&', '%26').replace('+', '%2B');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function artist_matcher(n, existing_artist) {
|
||||||
|
var existing_artist = existing_artist.toString();
|
||||||
|
var existing_artist_clean = existing_artist.replace('&', '%26').replace('+', '%2B');
|
||||||
|
$('#match_artists'+n).attr('data-success', 'Successfully matched '+existing_artist+' with '+selected_artist);
|
||||||
|
doAjaxCall('markUnmatched?action=matchArtist&existing_artist='+existing_artist_clean+'&new_artist='+selected_artist_clean, $('#match_artists'+n), 'page');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function load_AlbumArtist(clicked_id) {
|
||||||
|
n=clicked_id.replace("match_albums","");
|
||||||
|
var d = $("#album_dialog"+n).dialog();
|
||||||
|
d.dialog("option", "width", 700);
|
||||||
|
d.dialog("option", "position", "center");
|
||||||
|
$('#album_artist_options'+n).html('');
|
||||||
|
$.each(${json_artists}, function(key, value) {
|
||||||
|
$('#album_artist_options'+n).append($("<option/>", {
|
||||||
|
value: value,
|
||||||
|
text: value
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
load_json_albums(n)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function load_json_albums(n) {
|
||||||
|
selected_album_artist = $("#album_artist_options"+n).find("option:selected").text();
|
||||||
|
selected_album_artist_clean = selected_album_artist.replace('&', '%26').replace('+', '%2B');
|
||||||
|
$('#album_options'+n).html('')
|
||||||
|
$.getJSON("getAlbumsByArtist_json?artist="+selected_album_artist_clean, function( data ) {
|
||||||
|
$.each( data, function( key, value ) {
|
||||||
|
$('#album_options'+n).append($("<option/>", {
|
||||||
|
value: value,
|
||||||
|
text: value
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
selected_album= $("#album_options"+n).find("option:selected").text();
|
||||||
|
selected_album_clean = selected_album.replace('&', '%26').replace('+', '%2B');
|
||||||
|
});
|
||||||
|
change_json_albums(n)
|
||||||
|
change_album(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function change_json_albums(n) {
|
||||||
|
$("#album_artist_options"+n).change(function(){
|
||||||
|
selected_album_artist = $("#album_artist_options"+n).find("option:selected").text();
|
||||||
|
selected_album_artist_clean = selected_album_artist.replace('&', '%26').replace('+', '%2B');
|
||||||
|
$('#album_options'+n).html('')
|
||||||
|
$.getJSON("getAlbumsByArtist_json?artist="+selected_album_artist_clean, function( data ) {
|
||||||
|
$.each( data, function( key, value ) {
|
||||||
|
$('#album_options'+n).append($("<option/>", {
|
||||||
|
value: value,
|
||||||
|
text: value
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
selected_album= $("#album_options"+n).find("option:selected").text();
|
||||||
|
selected_album_clean = selected_album.replace('&', '%26').replace('+', '%2B');
|
||||||
|
});
|
||||||
|
change_album(n)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function change_album(n) {
|
||||||
|
$("#album_options"+n).change(function() {
|
||||||
|
selected_album= $("#album_options"+n).find("option:selected").text();
|
||||||
|
selected_album_clean = selected_album.replace('&', '%26').replace('+', '%2B');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function album_matcher(n, existing_artist, existing_album) {
|
||||||
|
var existing_artist = existing_artist.toString();
|
||||||
|
var existing_artist_clean = existing_artist.replace('&', '%26').replace('+', '%2B');
|
||||||
|
var existing_album = existing_album.toString();
|
||||||
|
var existing_album_clean = existing_album.replace('&', '%26').replace('+', '%2B');
|
||||||
|
$('#match_albums'+n).attr('data-success', 'Successfully matched '+existing_album+' with '+selected_album);
|
||||||
|
doAjaxCall('markUnmatched?action=matchAlbum&existing_artist='+existing_artist_clean+'&new_artist='+selected_album_artist_clean+'&existing_album='+existing_album_clean+'&new_album='+selected_album_clean, $('#match_albums'+n), 'page');
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|||||||
@@ -8,24 +8,24 @@
|
|||||||
<table class="display" id="searchresults_table">
|
<table class="display" id="searchresults_table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="column-albumart"></th> <%-- Changed ID to class --%>
|
<th id="albumart"></th>
|
||||||
%if type == 'album':
|
%if type == 'album':
|
||||||
<th class="column-albumname">Album Name</th>
|
<th id="albumname">Album Name</th>
|
||||||
<th class="column-artistname-small">Artist Name</th>
|
<th id="artistnamesmall">Artist Name</th>
|
||||||
<th class="column-format">Format</th>
|
<th id="format">Format</th>
|
||||||
<th class="column-tracks">Tracks</th>
|
<th id="tracks">Tracks</th>
|
||||||
<th class="column-reldate">Date</th>
|
<th id="reldate">Date</th>
|
||||||
<th class="column-score-small">Score</th>
|
<th id="scoresmall">Score</th>
|
||||||
<th class="column-mbrelid" style="display:none;"</th> <%-- Move display:none to CSS if always hidden --%>
|
<th id="mbrelid" style="display:none;"</th>
|
||||||
%elif type == 'artist':
|
%elif type == 'artist':
|
||||||
<th class="column-artistname">Artist Name</th>
|
<th id="artistname">Artist Name</th>
|
||||||
<th class="column-score">Score</th>
|
<th id="score">Score</th>
|
||||||
%else:
|
%else:
|
||||||
<th class="column-seriesname">Series Name</th>
|
<th id="seriesname">Series Name</th>
|
||||||
<th class="column-type">Type</th>
|
<th id="type">Type</th>
|
||||||
<th class="column-score">Score</th>
|
<th id="score">Score</th>
|
||||||
%endif
|
%endif
|
||||||
<th class="column-mb"></th>
|
<th id="mb"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -39,55 +39,34 @@
|
|||||||
|
|
||||||
if type == 'album':
|
if type == 'album':
|
||||||
albuminfo = 'Type: ' + result['rgtype'] + ', Country: ' + result['country']
|
albuminfo = 'Type: ' + result['rgtype'] + ', Country: ' + result['country']
|
||||||
# Constructing CAA URL for fallback, will be used as data-src for unveiling
|
|
||||||
caa_group_url = "http://coverartarchive.org/release-group/%s/front-250.jpg" %result['rgid']
|
caa_group_url = "http://coverartarchive.org/release-group/%s/front-250.jpg" %result['rgid']
|
||||||
# MusicBrainz album art URL (assuming it's a direct link to the image)
|
|
||||||
# If result['image'] provides a direct image URL, use it. Otherwise, rely on CAA or generic.
|
|
||||||
# For this modernization, let's assume getAlbumArtURL is available or build a generic one.
|
|
||||||
# For now, will prioritize getImageLinks.
|
|
||||||
%>
|
%>
|
||||||
<tr class="grade${grade}">
|
<tr class="grade${grade}">
|
||||||
%if type == 'album':
|
%if type == 'album':
|
||||||
<td class="column-albumart album-art-cell"> <%-- Changed ID to class --%>
|
<td id="albumart" style=" text-align: center; vertical-align: middle;"><div id="artistImg"><img title="${result['albumid']}" class="albumArt" height="50" width="50" onerror="tryCCA(this, '${caa_group_url}')"></div></td>
|
||||||
<div class="artwork-container">
|
|
||||||
<%-- Using data-src for lazy loading. onerror will handle the tryCCA fallback. --%>
|
|
||||||
<img title="${result['albumid']}" class="albumArt search-result-albumart"
|
|
||||||
data-src="" <%-- Will be populated by getImageLinks --%>
|
|
||||||
onerror="tryCCA(this, '${caa_group_url}', 'album')"
|
|
||||||
alt="Cover art for ${result['title']}" loading="lazy">
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
%elif type == 'artist':
|
%elif type == 'artist':
|
||||||
<td class="column-albumart artist-art-cell"> <%-- Changed ID to class --%>
|
<td id="albumart"><div id="artistImg"><img title="${result['id']}" class="albumArt" height="50" width="50"></div></td>
|
||||||
<div class="artwork-container">
|
|
||||||
<%-- Assuming artists might also have an image, or it will be a generic icon. --%>
|
|
||||||
<img title="${result['id']}" class="albumArt search-result-artistart"
|
|
||||||
data-src="" <%-- Will be populated by getImageLinks --%>
|
|
||||||
onerror="this.onerror=null;this.src='interfaces/default/images/no-cover-art.png';"
|
|
||||||
alt="Image for ${result['uniquename']}" loading="lazy">
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
%else:
|
%else:
|
||||||
<td class="column-albumart series-art-cell"></td> <%-- No artwork for series --%>
|
<td id="albumart"></td>
|
||||||
%endif
|
%endif
|
||||||
%if type == 'album':
|
%if type == 'album':
|
||||||
<td class="column-albumname"><a href="addReleaseById?rid=${result['albumid']}&rgid=${result['rgid']}" title="${albuminfo}">${result['title']}</a></td>
|
<td id="albumname"><a href="addReleaseById?rid=${result['albumid']}&rgid=${result['rgid']}" title="${albuminfo}">${result['title']}</a></td>
|
||||||
<td class="column-artistname-small"><a href="addArtist?artistid=${result['id']}" title="${result['uniquename']}">${result['uniquename']}</a></td>
|
<td id="artistnamesmall"><a href="addArtist?artistid=${result['id']}" title="${result['uniquename']}">${result['uniquename']}</a></td>
|
||||||
<td class="column-format">${result['formats']}</td>
|
<td id="format">${result['formats']}</td>
|
||||||
<td class="column-tracks">${result['tracks']}</td>
|
<td id="tracks">${result['tracks']}</td>
|
||||||
<td class="column-reldate">${result['date']}</td>
|
<td id="reldate">${result['date']}</td>
|
||||||
<td class="column-score"><a href="${result['albumurl']}" title="View on MusicBrainz"><div class="bar"><div class="score" style="width: ${result['score']}px">${result['score']}</div></div></a></td>
|
<td id="score"><a href="${result['albumurl']} "title="View on MusicBrainz"><div class="bar"><div class="score" style="width: ${result['score']}px">${result['score']}</div></div></a></td>
|
||||||
<td class="column-musicbrainz musicbrainz-link-cell"><a href="${result['albumurl']}" target="_blank" rel="noopener noreferrer"><img src="interfaces/default/images/MusicBrainz_Album_Icon.png" title="View on MusicBrainz" height="20" width="20" alt="MusicBrainz Link"></a></td>
|
<td id="musicbrainz" style=" text-align: center; line-height: 0; vertical-align: middle;"><a href="${result['albumurl']}"><img src="interfaces/default/images/MusicBrainz_Album_Icon.png" title="View on MusicBrainz" height="20" width="20"></a></td>
|
||||||
<td class="column-mbrelid" style="display:none;">${result['albumid']}</td> <%-- Move display:none to CSS if always hidden --%>
|
<td id="mbrelid" style="display:none;">${result['albumid']}</td>
|
||||||
%elif type == 'artist':
|
%elif type == 'artist':
|
||||||
<td class="column-artistname"><a href="addArtist?artistid=${result['id']}" title="${result['uniquename']}">${result['uniquename']}</a></td>
|
<td id="artistname"><a href="addArtist?artistid=${result['id']}" title="${result['uniquename']}">${result['uniquename']}</a></td>
|
||||||
<td class="column-score"><a href="${result['url']}" title="View on MusicBrainz"><div class="bar"><div class="score" style="width: ${result['score']}px">${result['score']}</div></div></a></td>
|
<td id="score"><a href="${result['url']} "title="View on MusicBrainz"><div class="bar"><div class="score" style="width: ${result['score']}px">${result['score']}</div></div></a></td>
|
||||||
<td class="column-musicbrainz musicbrainz-link-cell"><a href="${result['url']}" target="_blank" rel="noopener noreferrer"><img src="interfaces/default/images/MusicBrainz_Artist_Icon.png" title="View on MusicBrainz" height="20" width="20" alt="MusicBrainz Link"></a></td>
|
<td id="musicbrainz" style=" text-align: center; line-height: 0; vertical-align: middle;"><a href="${result['url']}"><img src="interfaces/default/images/MusicBrainz_Artist_Icon.png" title="View on MusicBrainz" height="20" width="20"></a></td>
|
||||||
%else:
|
%else:
|
||||||
<td class="column-seriesname"><a href="addSeries?seriesid=${result['id']}" title="${result['uniquename']}">${result['uniquename']}</a></td>
|
<td id="seriesname"><a href="addSeries?seriesid=${result['id']}" title="${result['uniquename']}">${result['uniquename']}</a></td>
|
||||||
<td class="column-type">${result['type']}</td>
|
<td id="type">${result['type']}</td>
|
||||||
<td class="column-score"><a href="${result['url']}" title="View on MusicBrainz"><div class="bar"><div class="score" style="width: ${result['score']}px">${result['score']}</div></div></a></td>
|
<td id="score"><a href="${result['url']} "title="View on MusicBrainz"><div class="bar"><div class="score" style="width: ${result['score']}px">${result['score']}</div></div></a></td>
|
||||||
<td class="column-musicbrainz musicbrainz-link-cell"><a href="${result['url']}" target="_blank" rel="noopener noreferrer"><img src="interfaces/default/images/MusicBrainz_Artist_Icon.png" title="View on MusicBrainz" height="20" width="20" alt="MusicBrainz Link"></a></td>
|
<td id="musicbrainz" style=" text-align: center; line-height: 0; vertical-align: middle;"><a href="${result['url']}"><img src="interfaces/default/images/MusicBrainz_Artist_Icon.png" title="View on MusicBrainz" height="20" width="20"></a></td>
|
||||||
%endif
|
%endif
|
||||||
</tr>
|
</tr>
|
||||||
%endfor
|
%endfor
|
||||||
@@ -98,77 +77,37 @@
|
|||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="headIncludes()">
|
<%def name="headIncludes()">
|
||||||
${parent.headIncludes()} <%-- Ensure parent head includes are kept --%>
|
|
||||||
<link rel="stylesheet" href="interfaces/default/css/data_table.css">
|
<link rel="stylesheet" href="interfaces/default/css/data_table.css">
|
||||||
<style>
|
|
||||||
/* Basic CSS for album art thumbnail, consistent with other pages */
|
|
||||||
.search-result-albumart, .search-result-artistart {
|
|
||||||
height: 50px;
|
|
||||||
width: 50px;
|
|
||||||
object-fit: cover; /* Ensures image covers the area without distortion */
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* MusicBrainz icon styling */
|
|
||||||
.musicbrainz-link-cell img {
|
|
||||||
height: 20px;
|
|
||||||
width: 20px;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Score bar styling (if not already in data_table.css) */
|
|
||||||
.bar {
|
|
||||||
width: 100px; /* Or a fixed width */
|
|
||||||
background-color: #eee;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
height: 18px; /* Adjust as needed */
|
|
||||||
line-height: 18px;
|
|
||||||
text-align: right;
|
|
||||||
border-radius: 3px;
|
|
||||||
overflow: hidden; /* Hide overflow if score > 100px */
|
|
||||||
}
|
|
||||||
.score {
|
|
||||||
height: 100%;
|
|
||||||
background-color: #5cb85c; /* Green for score */
|
|
||||||
color: #fff;
|
|
||||||
padding-right: 5px;
|
|
||||||
font-size: 0.8em;
|
|
||||||
box-sizing: border-box; /* Include padding in width calculation */
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="javascriptIncludes()">
|
<%def name="javascriptIncludes()">
|
||||||
${parent.javascriptIncludes()} <%-- Ensure parent javascript includes are kept --%>
|
|
||||||
<script src="js/libs/jquery.unveil.min.js"></script>
|
<script src="js/libs/jquery.unveil.min.js"></script>
|
||||||
<script src="js/libs/jquery.dataTables.min.js"></script>
|
<script src="js/libs/jquery.dataTables.min.js"></script>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
// Global function to try Cover Art Archive or generic fallback
|
function tryCCA(element, url) {
|
||||||
function tryCCA(element, url, type) {
|
|
||||||
element.onerror = function() {
|
element.onerror = function() {
|
||||||
element.onerror = null; // Prevent infinite loops
|
element.onerror = null;
|
||||||
element.src = "interfaces/default/images/no-cover-art.png"; // Fallback generic image
|
element.src = "interfaces/default/images/no-cover-art.png";
|
||||||
if (type === 'artist') {
|
|
||||||
// Specific fallback for artists if different
|
|
||||||
// element.src = "interfaces/default/images/no-artist-art.png";
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
if (url) {
|
element.src = url;
|
||||||
element.src = url; // Try the provided URL (e.g., CAA)
|
|
||||||
} else {
|
|
||||||
element.src = "interfaces/default/images/no-cover-art.png"; // Fallback if no specific URL provided
|
|
||||||
}
|
}
|
||||||
|
function getArt() {
|
||||||
|
$("table#searchresults_table tr td#albumart img").each(function(){
|
||||||
|
var id = $(this).attr('title');
|
||||||
|
var image = $(this);
|
||||||
|
|
||||||
|
if (!image.hasClass('done')) {
|
||||||
|
image.addClass('done');
|
||||||
|
getImageLinks(image, id, "${type}", true);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
// Encapsulate page-specific logic
|
}
|
||||||
var SearchResultsPage = SearchResultsPage || {};
|
function initThisPage() {
|
||||||
|
|
||||||
SearchResultsPage.initDataTable = function() {
|
|
||||||
$('#searchresults_table').dataTable({
|
$('#searchresults_table').dataTable({
|
||||||
"bDestroy": true,
|
"bDestroy": true,
|
||||||
"aoColumnDefs": [
|
"aoColumnDefs": [
|
||||||
{ 'bSortable': false, 'aTargets': [ 0, 7 ] } // Disable sorting for albumart (0) and MusicBrainz icon (7) columns
|
{ 'bSortable': false, 'aTargets': [ 0 ] }
|
||||||
],
|
],
|
||||||
"oLanguage": {
|
"oLanguage": {
|
||||||
"sLengthMenu":"Show _MENU_ results per page",
|
"sLengthMenu":"Show _MENU_ results per page",
|
||||||
@@ -179,62 +118,36 @@
|
|||||||
"sSearch" : ""},
|
"sSearch" : ""},
|
||||||
"iDisplayLength": 25,
|
"iDisplayLength": 25,
|
||||||
"sPaginationType": "full_numbers",
|
"sPaginationType": "full_numbers",
|
||||||
"aaSorting": [], // No initial sorting specified, DataTables default
|
"aaSorting": [],
|
||||||
"fnDrawCallback": function (o) {
|
"fnDrawCallback": function (o) {
|
||||||
// Jump to top of page
|
// Jump to top of page
|
||||||
$('html,body').scrollTop(0);
|
$('html,body').scrollTop(0);
|
||||||
// Re-unveil images after each draw for lazy loading
|
|
||||||
$("img.search-result-albumart, img.search-result-artistart").unveil();
|
|
||||||
// Re-populate art for new rows
|
|
||||||
SearchResultsPage.getArt();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
$('#searchresults_table').on("draw.dt", function () {
|
||||||
|
getArt();
|
||||||
// Custom function to get image links and apply them to data-src
|
$("img").unveil();
|
||||||
SearchResultsPage.getArt = function() {
|
|
||||||
// Target all relevant image elements that are not yet "done"
|
|
||||||
$("img.search-result-albumart:not(.done), img.search-result-artistart:not(.done)").each(function(){
|
|
||||||
var $image = $(this);
|
|
||||||
var id = $image.attr('title'); // Use title as ID for getImageLinks
|
|
||||||
|
|
||||||
// Add 'done' class immediately to prevent re-processing
|
|
||||||
$image.addClass('done');
|
|
||||||
|
|
||||||
// Assuming getImageLinks is a global function that fetches the actual image URL
|
|
||||||
// and sets it to the data-src attribute.
|
|
||||||
// If getImageLinks directly sets the src, unveil() will still work, but
|
|
||||||
// this setup aims for data-src first.
|
|
||||||
if (typeof getImageLinks === 'function') {
|
|
||||||
// getImageLinks should ideally return a promise or accept a callback
|
|
||||||
// and then set $image.attr('data-src', actualImageUrl);
|
|
||||||
// For now, assuming it handles it internally or synchronously.
|
|
||||||
getImageLinks($image, id, "${type}", true); // Assuming this sets data-src or src
|
|
||||||
} else {
|
|
||||||
console.warn("getImageLinks function is not defined. Image loading might be impacted.");
|
|
||||||
// Fallback to a generic image if getImageLinks is not available and no data-src is set.
|
|
||||||
// This might be handled by onerror, but adding here for explicit safety.
|
|
||||||
if (!$image.attr('data-src') && !$image.attr('src')) {
|
|
||||||
$image.attr('src', "interfaces/default/images/no-cover-art.png");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
getArt();
|
||||||
|
resetFilters("album");
|
||||||
|
}
|
||||||
$(document).ready(function(){
|
$(document).ready(function(){
|
||||||
initFancybox(); // Assuming initFancybox is a global function
|
initFancybox();
|
||||||
SearchResultsPage.initDataTable();
|
initThisPage();
|
||||||
SearchResultsPage.getArt(); // Initial call to load images on page load
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
// This part sets search parameters based on backend data.
|
<script type="text/javascript">
|
||||||
// Assuming searchbar and its inputs exist globally.
|
<%!
|
||||||
|
# Abuse JSON module for escaping JavaScript
|
||||||
|
import json
|
||||||
|
%>
|
||||||
|
$(document).ready(function() {
|
||||||
|
// Search parameter
|
||||||
$("#searchbar input[name=name]").val(${name | json.dumps});
|
$("#searchbar input[name=name]").val(${name | json.dumps});
|
||||||
$("#searchbar select[name=type]").val(${type | json.dumps});
|
|
||||||
|
|
||||||
// Assuming resetFilters is a global function from common.js
|
// Album or artist
|
||||||
if (typeof resetFilters === 'function') {
|
$("#searchbar select[name=type]").val(${type | json.dumps});
|
||||||
resetFilters("album"); // Or adjust based on 'type' if needed
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|||||||
@@ -1,44 +1,12 @@
|
|||||||
<%inherit file="base.html"/>
|
<%inherit file="base.html"/>
|
||||||
|
|
||||||
<%def name="headIncludes()">
|
<%def name="headIncludes()">
|
||||||
${parent.headIncludes()} <%-- Ensure parent head includes are kept --%>
|
|
||||||
<meta http-equiv="refresh" content="${timer};url=index">
|
<meta http-equiv="refresh" content="${timer};url=index">
|
||||||
<style>
|
|
||||||
/* Basic styling for the shutdown message */
|
|
||||||
#shutdown-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 100vh; /* Full viewport height */
|
|
||||||
text-align: center;
|
|
||||||
background-color: #f8f8f8; /* Light background */
|
|
||||||
color: #333;
|
|
||||||
font-family: sans-serif; /* A common, readable font */
|
|
||||||
}
|
|
||||||
|
|
||||||
#shutdown-message h1 {
|
|
||||||
font-size: 2.5em; /* Larger, more prominent text */
|
|
||||||
color: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
#shutdown-message i {
|
|
||||||
margin-right: 15px; /* Space between icon and text */
|
|
||||||
color: #4CAF50; /* A green color for success/active states */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* If it's truly shutting down or restarting, maybe a different color */
|
|
||||||
#shutdown-message.restarting i {
|
|
||||||
color: #ff9800; /* Orange for restart */
|
|
||||||
}
|
|
||||||
#shutdown-message.shutting-down i {
|
|
||||||
color: #f44336; /* Red for shutdown */
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="body()">
|
<%def name="body()">
|
||||||
<div class="table_wrapper" id="shutdown-container"> <%-- Added a container for centering --%>
|
<div class="table_wrapper">
|
||||||
<div id="shutdown-message" class="${'restarting' if message == 'restarting' else 'shutting-down'}"> <%-- Added class for conditional styling --%>
|
<div id="shutdown">
|
||||||
<h1><i class="fa fa-refresh fa-spin"></i> Headphones is ${message}</h1>
|
<h1><i class="fa fa-refresh fa-spin"></i> Headphones is ${message}</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,10 +3,7 @@
|
|||||||
<%def name="headerIncludes()">
|
<%def name="headerIncludes()">
|
||||||
<div id="subhead_container">
|
<div id="subhead_container">
|
||||||
<div id="subhead_menu">
|
<div id="subhead_menu">
|
||||||
<%-- Changed inline onclick to a class and data attributes for JS handling --%>
|
<a href="javascript:void(0)" id="menu_link_scan" onclick="doAjaxCall('forceSearch',$(this))" data-success="Checking for wanted albums successful" data-error="Error checking wanted albums"><i class="fa fa-search"></i> Force Check</a>
|
||||||
<a href="#" id="menu_link_force_check" class="ajax-link" data-action="forceSearch" data-success="Checking for wanted albums successful" data-error="Error checking wanted albums">
|
|
||||||
<i class="fa fa-search"></i> Force Check
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</%def>
|
</%def>
|
||||||
@@ -16,44 +13,38 @@
|
|||||||
<div id="paddingheader">
|
<div id="paddingheader">
|
||||||
<h1 class="clearfix"><i class="fa fa-heart"></i> Wanted Albums</h1>
|
<h1 class="clearfix"><i class="fa fa-heart"></i> Wanted Albums</h1>
|
||||||
</div>
|
</div>
|
||||||
<form action="markAlbums" method="get" id="markAlbumsForm"> <%-- Added ID to the form --%>
|
<form action="markAlbums" method="get" id="markAlbums">
|
||||||
<div id="mark_albums_controls"> <%-- Unique and descriptive ID, moved inline style to CSS --%>
|
<div id="markalbum" style="top:0;">
|
||||||
Mark selected albums as
|
Mark selected albums as
|
||||||
<select name="action" id="markAlbumActionSelect"> <%-- Added ID --%>
|
<select name="action" onChange="doAjaxCall('markAlbums',$(this),'table',true);" data-error="You didn't select any albums">
|
||||||
<option disabled="disabled" selected="selected">Choose...</option>
|
<option disabled="disabled" selected="selected">Choose...</option>
|
||||||
<option value="Skipped">Skipped</option>
|
<option value="Skipped">Skipped</option>
|
||||||
<option value="Ignored">Ignored</option>
|
<option value="Ignored">Ignored</option>
|
||||||
<option value="Downloaded">Downloaded</option>
|
<option value="Downloaded">Downloaded</option>
|
||||||
</select>
|
</select>
|
||||||
<%-- Replaced hidden input with a visible button if desired, or handle submit via JS --%>
|
<input type="hidden" value="Go">
|
||||||
<%-- For now, keep the JS-driven approach as close as possible to original logic --%>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="table_wrapper" id="wanted_table_wrapper" >
|
<div class="table_wrapper" id="wanted_table_wrapper" >
|
||||||
<table class="display" id="wanted_table">
|
<table class="display" id="wanted_table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="select-all-checkbox"><input type="checkbox" id="toggleAllWanted" /></th> <%-- Added ID for easier targeting --%>
|
<th id="select"><input type="checkbox" onClick="toggle(this)" /></th>
|
||||||
<th class="column-albumart"></th>
|
<th id="albumart"></th>
|
||||||
<th class="column-artistname">Artist</th>
|
<th id="artistname">Artist</th>
|
||||||
<th class="column-albumname">Album Name</th>
|
<th id="albumname">Album Name</th>
|
||||||
<th class="column-reldate">Release Date</th>
|
<th id="reldate">Release Date</th>
|
||||||
<th class="column-type">Type</th>
|
<th id="type">Type</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
%for album in wanted:
|
%for album in wanted:
|
||||||
<tr class="gradeZ">
|
<tr class="gradeZ">
|
||||||
<td class="select-checkbox"><input type="checkbox" name="album_ids" value="${album['AlbumID']}" class="wanted-album-checkbox" /></td> <%-- Changed name to album_ids, added class --%>
|
<td id="select"><input type="checkbox" name="${album['AlbumID']}" class="checkbox" /></th>
|
||||||
<td class="column-albumart">
|
<td id="albumart"><img title="${album['AlbumID']}" height="64" width="64" src="interfaces/default/images/no-cover-art.png" data-src="artwork/thumbs/album/${album['AlbumID']}"></td>
|
||||||
<img title="${album['AlbumID']}" height="64" width="64"
|
<td id="artistname"><a href="artistPage?ArtistID=${album['ArtistID']}">${album['ArtistName']}</a></td>
|
||||||
data-src="artwork/thumbs/album/${album['AlbumID']}"
|
<td id="albumname"><a href="albumPage?AlbumID=${album['AlbumID']}">${album['AlbumTitle']}</a></td>
|
||||||
src="interfaces/default/images/no-cover-art.png" <%-- Fallback src --%>
|
<td id="reldate">${album['ReleaseDate']}</td>
|
||||||
alt="Cover art for ${album['AlbumTitle']}" loading="lazy">
|
<td id="type">${album['Type']}</td>
|
||||||
</td>
|
|
||||||
<td class="column-artistname"><a href="artistPage?ArtistID=${album['ArtistID']}">${album['ArtistName']}</a></td>
|
|
||||||
<td class="column-albumname"><a href="albumPage?AlbumID=${album['AlbumID']}">${album['AlbumTitle']}</a></td>
|
|
||||||
<td class="column-reldate">${album['ReleaseDate']}</td>
|
|
||||||
<td class="column-type">${album['Type']}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
%endfor
|
%endfor
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -65,31 +56,26 @@
|
|||||||
<h1 class="clearfix"><i class="fa fa-calendar"></i> Upcoming Albums</h1>
|
<h1 class="clearfix"><i class="fa fa-calendar"></i> Upcoming Albums</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="table_wrapper">
|
<div class="table_wrapper">
|
||||||
<table class="display_no_select" id="upcoming_table"> <%-- No changes here, but ensuring it gets DataTables init --%>
|
<table class="display_no_select" id="upcoming_table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="column-albumart"></th>
|
<th id="albumart"></th>
|
||||||
<th class="column-artistname">Artist</th>
|
<th id="artistname">Artist</th>
|
||||||
<th class="column-albumname">Album Name</th>
|
<th id="albumname">Album Name</th>
|
||||||
<th class="column-reldate">Release Date</th>
|
<th id="reldate">Release Date</th>
|
||||||
<th class="column-type">Type</th>
|
<th id="type">Type</th>
|
||||||
<th class="column-status">Status</th>
|
<th id="status">Status</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
%for album in upcoming:
|
%for album in upcoming:
|
||||||
<tr class="gradeZ">
|
<tr class="gradeZ">
|
||||||
<td class="column-albumart">
|
<td id="albumart"><img title="${album['AlbumID']}" height="64" width="64" data-src="artwork/thumbs/album/${album['AlbumID']}"></td>
|
||||||
<img title="${album['AlbumID']}" height="64" width="64"
|
<td id="artistname"><a href="artistPage?ArtistID=${album['ArtistID']}">${album['ArtistName']}</a></td>
|
||||||
data-src="artwork/thumbs/album/${album['AlbumID']}"
|
<td id="albumname"><a href="albumPage?AlbumID=${album['AlbumID']}">${album['AlbumTitle']}</a></td>
|
||||||
src="interfaces/default/images/no-cover-art.png" <%-- Fallback src --%>
|
<td id="reldate">${album['ReleaseDate']}</td>
|
||||||
alt="Cover art for ${album['AlbumTitle']}" loading="lazy">
|
<td id="type">${album['Type']}</td>
|
||||||
</td>
|
<td id="status">${album['Status']}</td>
|
||||||
<td class="column-artistname"><a href="artistPage?ArtistID=${album['ArtistID']}">${album['ArtistName']}</a></td>
|
|
||||||
<td class="column-albumname"><a href="albumPage?AlbumID=${album['AlbumID']}">${album['AlbumTitle']}</a></td>
|
|
||||||
<td class="column-reldate">${album['ReleaseDate']}</td>
|
|
||||||
<td class="column-type">${album['Type']}</td>
|
|
||||||
<td class="column-status">${album['Status']}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
%endfor
|
%endfor
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -98,154 +84,29 @@
|
|||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="headIncludes()">
|
<%def name="headIncludes()">
|
||||||
${parent.headIncludes()} <%-- Ensure parent head includes are kept --%>
|
|
||||||
<link rel="stylesheet" href="interfaces/default/css/data_table.css">
|
<link rel="stylesheet" href="interfaces/default/css/data_table.css">
|
||||||
<style>
|
|
||||||
#mark_albums_controls {
|
|
||||||
position: relative; /* Adjust as per original 'top:0;' intent */
|
|
||||||
margin-bottom: 15px; /* Add some space below controls */
|
|
||||||
}
|
|
||||||
/* Style for album art thumbnails */
|
|
||||||
.column-albumart img {
|
|
||||||
width: 64px;
|
|
||||||
height: 64px;
|
|
||||||
object-fit: cover; /* Ensures image fills the space without distortion */
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="javascriptIncludes()">
|
<%def name="javascriptIncludes()">
|
||||||
${parent.javascriptIncludes()} <%-- Ensure parent javascript includes are kept --%>
|
|
||||||
<script src="js/libs/jquery.unveil.min.js"></script>
|
<script src="js/libs/jquery.unveil.min.js"></script>
|
||||||
<script src="js/libs/jquery.dataTables.min.js"></script>
|
<script src="js/libs/jquery.dataTables.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Encapsulate page-specific logic
|
function initThisPage() {
|
||||||
var UpcomingPage = UpcomingPage || {};
|
$("img").unveil();
|
||||||
|
|
||||||
UpcomingPage.initDataTables = function() {
|
|
||||||
// Initialize Wanted Albums table
|
|
||||||
$('#wanted_table').dataTable({
|
$('#wanted_table').dataTable({
|
||||||
"oLanguage": {
|
"oLanguage": {
|
||||||
"sEmptyTable": "No wanted albums found" // More descriptive empty table message
|
"sEmptyTable": " "
|
||||||
},
|
},
|
||||||
"bDestroy": true,
|
"bDestroy": true,
|
||||||
"bFilter": false,
|
"bFilter": false,
|
||||||
"bInfo": false,
|
"bInfo": false,
|
||||||
"bPaginate": false,
|
"bPaginate": false
|
||||||
"aoColumnDefs": [
|
|
||||||
{ 'bSortable': false, 'aTargets': [ 0, 1 ] } // Disable sorting for checkbox and album art columns
|
|
||||||
],
|
|
||||||
"fnDrawCallback": function (o) {
|
|
||||||
// Re-unveil images after each draw for lazy loading
|
|
||||||
$("img[data-src]").unveil();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
resetFilters("artists");
|
||||||
// Initialize Upcoming Albums table
|
|
||||||
$('#upcoming_table').dataTable({
|
|
||||||
"oLanguage": {
|
|
||||||
"sEmptyTable": "No upcoming albums found" // More descriptive empty table message
|
|
||||||
},
|
|
||||||
"bDestroy": true,
|
|
||||||
"bFilter": false,
|
|
||||||
"bInfo": false,
|
|
||||||
"bPaginate": false,
|
|
||||||
"aoColumnDefs": [
|
|
||||||
{ 'bSortable': false, 'aTargets': [ 0 ] } // Disable sorting for album art column
|
|
||||||
],
|
|
||||||
"aaSorting": [[3, 'asc']], // Sort by Release Date ascending by default
|
|
||||||
"fnDrawCallback": function (o) {
|
|
||||||
// Re-unveil images after each draw for lazy loading
|
|
||||||
$("img[data-src]").unveil();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
UpcomingPage.initActions = function() {
|
|
||||||
// Event listener for "Force Check" link
|
|
||||||
$('#menu_link_force_check').on('click', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
var $this = $(this);
|
|
||||||
var actionUrl = $this.data('action'); // 'forceSearch'
|
|
||||||
var successMsg = $this.data('success');
|
|
||||||
var errorMsg = $this.data('error');
|
|
||||||
|
|
||||||
if (typeof doAjaxCall === 'function') {
|
|
||||||
doAjaxCall(actionUrl, $this, 'page', successMsg, errorMsg);
|
|
||||||
} else {
|
|
||||||
console.error("doAjaxCall function is not defined. Cannot force check.");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Event listener for "Mark selected albums as" dropdown
|
|
||||||
$('#markAlbumActionSelect').on('change', function() {
|
|
||||||
var $this = $(this);
|
|
||||||
var action = $this.val();
|
|
||||||
var selectedAlbumIds = [];
|
|
||||||
|
|
||||||
$('.wanted-album-checkbox:checked').each(function() {
|
|
||||||
selectedAlbumIds.push($(this).val());
|
|
||||||
});
|
|
||||||
|
|
||||||
if (selectedAlbumIds.length === 0) {
|
|
||||||
if (typeof showMessage === 'function') {
|
|
||||||
showMessage($this.data('error') || "You didn't select any albums", 'error');
|
|
||||||
} else {
|
|
||||||
alert($this.data('error') || "You didn't select any albums");
|
|
||||||
}
|
|
||||||
// Reset dropdown to "Choose..."
|
|
||||||
$this.val('Choose...');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var url = $('#markAlbumsForm').attr('action') + '?action=' + encodeURIComponent(action);
|
|
||||||
$.each(selectedAlbumIds, function(index, id) {
|
|
||||||
url += '&album_ids=' + encodeURIComponent(id); // Append selected album IDs
|
|
||||||
});
|
|
||||||
|
|
||||||
var successMsg = 'Albums successfully marked as ' + action;
|
|
||||||
var errorMsg = 'Error marking albums.';
|
|
||||||
|
|
||||||
if (typeof doAjaxCall === 'function') {
|
|
||||||
doAjaxCall(url, $this, 'table', successMsg, errorMsg);
|
|
||||||
// Reset dropdown after action
|
|
||||||
$this.val('Choose...');
|
|
||||||
} else {
|
|
||||||
console.error("doAjaxCall function is not defined. Cannot mark albums.");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Event listener for the "toggle all" checkbox in Wanted Albums table
|
|
||||||
$('#toggleAllWanted').on('click', function() {
|
|
||||||
$('.wanted-album-checkbox').prop('checked', this.checked);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update "toggle all" checkbox based on individual checkboxes
|
|
||||||
$(document).on('click', '.wanted-album-checkbox', function() {
|
|
||||||
if (!this.checked) {
|
|
||||||
$('#toggleAllWanted').prop('checked', false);
|
|
||||||
} else {
|
|
||||||
if ($('.wanted-album-checkbox:checked').length === $('.wanted-album-checkbox').length) {
|
|
||||||
$('#toggleAllWanted').prop('checked', true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assuming resetFilters and initActions are global functions from common.js
|
|
||||||
if (typeof resetFilters === 'function') {
|
|
||||||
resetFilters("artists"); // Or adjust based on context
|
|
||||||
}
|
|
||||||
if (typeof initActions === 'function') {
|
|
||||||
initActions();
|
initActions();
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
UpcomingPage.initDataTables();
|
initThisPage();
|
||||||
UpcomingPage.initActions();
|
|
||||||
// Initial unveil call for images
|
|
||||||
$("img[data-src]").unveil();
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|||||||
@@ -54,9 +54,6 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'BITRATE': (int, 'General', 192),
|
'BITRATE': (int, 'General', 192),
|
||||||
'BLACKHOLE': (int, 'General', 0),
|
'BLACKHOLE': (int, 'General', 0),
|
||||||
'BLACKHOLE_DIR': (path, 'General', ''),
|
'BLACKHOLE_DIR': (path, 'General', ''),
|
||||||
'BOXCAR_ENABLED': (int, 'Boxcar', 0),
|
|
||||||
'BOXCAR_ONSNATCH': (int, 'Boxcar', 0),
|
|
||||||
'BOXCAR_TOKEN': (str, 'Boxcar', ''),
|
|
||||||
'CACHE_DIR': (path, 'General', ''),
|
'CACHE_DIR': (path, 'General', ''),
|
||||||
'CACHE_SIZEMB': (int, 'Advanced', 32),
|
'CACHE_SIZEMB': (int, 'Advanced', 32),
|
||||||
'CHECK_GITHUB': (int, 'General', 1),
|
'CHECK_GITHUB': (int, 'General', 1),
|
||||||
@@ -125,10 +122,6 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'GIT_BRANCH': (str, 'General', 'master'),
|
'GIT_BRANCH': (str, 'General', 'master'),
|
||||||
'GIT_PATH': (path, 'General', ''),
|
'GIT_PATH': (path, 'General', ''),
|
||||||
'GIT_USER': (str, 'General', 'rembo10'),
|
'GIT_USER': (str, 'General', 'rembo10'),
|
||||||
'GROWL_ENABLED': (int, 'Growl', 0),
|
|
||||||
'GROWL_HOST': (str, 'Growl', ''),
|
|
||||||
'GROWL_ONSNATCH': (int, 'Growl', 0),
|
|
||||||
'GROWL_PASSWORD': (str, 'Growl', ''),
|
|
||||||
'HEADPHONES_INDEXER': (bool_int, 'General', False),
|
'HEADPHONES_INDEXER': (bool_int, 'General', False),
|
||||||
'HPPASS': (str, 'General', ''),
|
'HPPASS': (str, 'General', ''),
|
||||||
'HPUSER': (str, 'General', ''),
|
'HPUSER': (str, 'General', ''),
|
||||||
@@ -197,7 +190,6 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'OMGWTFNZBS_UID': (str, 'omgwtfnzbs', ''),
|
'OMGWTFNZBS_UID': (str, 'omgwtfnzbs', ''),
|
||||||
'OPEN_MAGNET_LINKS': (int, 'General', 0), # 0: Ignore, 1: Open, 2: Convert, 3: Embed (rtorrent)
|
'OPEN_MAGNET_LINKS': (int, 'General', 0), # 0: Ignore, 1: Open, 2: Convert, 3: Embed (rtorrent)
|
||||||
'MAGNET_LINKS': (int, 'General', 0),
|
'MAGNET_LINKS': (int, 'General', 0),
|
||||||
'OSX_NOTIFY_APP': (str, 'OSX_Notify', '/Applications/Headphones'),
|
|
||||||
'OSX_NOTIFY_ENABLED': (int, 'OSX_Notify', 0),
|
'OSX_NOTIFY_ENABLED': (int, 'OSX_Notify', 0),
|
||||||
'OSX_NOTIFY_ONSNATCH': (int, 'OSX_Notify', 0),
|
'OSX_NOTIFY_ONSNATCH': (int, 'OSX_Notify', 0),
|
||||||
'PIRATEBAY': (int, 'Piratebay', 0),
|
'PIRATEBAY': (int, 'Piratebay', 0),
|
||||||
|
|||||||
@@ -160,6 +160,15 @@ def encode(albumPath):
|
|||||||
|
|
||||||
# Use multicore if enabled
|
# Use multicore if enabled
|
||||||
if headphones.CONFIG.ENCODER_MULTICORE:
|
if headphones.CONFIG.ENCODER_MULTICORE:
|
||||||
|
|
||||||
|
# Set macOS multiprocessing method
|
||||||
|
try:
|
||||||
|
if headphones.SYS_PLATFORM == "darwin":
|
||||||
|
multiprocessing.set_start_method('fork')
|
||||||
|
except RuntimeError:
|
||||||
|
# Already set, ignore
|
||||||
|
pass
|
||||||
|
|
||||||
if headphones.CONFIG.ENCODER_MULTICORE_COUNT == 0:
|
if headphones.CONFIG.ENCODER_MULTICORE_COUNT == 0:
|
||||||
processes = multiprocessing.cpu_count()
|
processes = multiprocessing.cpu_count()
|
||||||
else:
|
else:
|
||||||
|
|||||||
+4
-185
@@ -15,98 +15,10 @@ from headphones import logger, helpers, common, request
|
|||||||
from pynma import pynma
|
from pynma import pynma
|
||||||
import cherrypy
|
import cherrypy
|
||||||
import headphones
|
import headphones
|
||||||
import gntp.notifier
|
|
||||||
#import oauth2 as oauth
|
#import oauth2 as oauth
|
||||||
import twitter
|
import twitter
|
||||||
|
|
||||||
|
|
||||||
class GROWL(object):
|
|
||||||
"""
|
|
||||||
Growl notifications, for OS X.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.enabled = headphones.CONFIG.GROWL_ENABLED
|
|
||||||
self.host = headphones.CONFIG.GROWL_HOST
|
|
||||||
self.password = headphones.CONFIG.GROWL_PASSWORD
|
|
||||||
|
|
||||||
def conf(self, options):
|
|
||||||
return cherrypy.config['config'].get('Growl', options)
|
|
||||||
|
|
||||||
def notify(self, message, event):
|
|
||||||
if not self.enabled:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Split host and port
|
|
||||||
if self.host == "":
|
|
||||||
host, port = "localhost", 23053
|
|
||||||
|
|
||||||
if ":" in self.host:
|
|
||||||
host, port = self.host.split(':', 1)
|
|
||||||
port = int(port)
|
|
||||||
else:
|
|
||||||
host, port = self.host, 23053
|
|
||||||
|
|
||||||
# If password is empty, assume none
|
|
||||||
if self.password == "":
|
|
||||||
password = None
|
|
||||||
else:
|
|
||||||
password = self.password
|
|
||||||
|
|
||||||
# Register notification
|
|
||||||
growl = gntp.notifier.GrowlNotifier(
|
|
||||||
applicationName='Headphones',
|
|
||||||
notifications=['New Event'],
|
|
||||||
defaultNotifications=['New Event'],
|
|
||||||
hostname=host,
|
|
||||||
port=port,
|
|
||||||
password=password
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
growl.register()
|
|
||||||
except gntp.notifier.errors.NetworkError:
|
|
||||||
logger.warning('Growl notification failed: network error')
|
|
||||||
return
|
|
||||||
except gntp.notifier.errors.AuthError:
|
|
||||||
logger.warning('Growl notification failed: authentication error')
|
|
||||||
return
|
|
||||||
|
|
||||||
# Fix message
|
|
||||||
message = message.encode(headphones.SYS_ENCODING, "replace")
|
|
||||||
|
|
||||||
# Send it, including an image
|
|
||||||
image_file = os.path.join(str(headphones.PROG_DIR),
|
|
||||||
"data/images/headphoneslogo.png")
|
|
||||||
|
|
||||||
with open(image_file, 'rb') as f:
|
|
||||||
image = f.read()
|
|
||||||
|
|
||||||
try:
|
|
||||||
growl.notify(
|
|
||||||
noteType='New Event',
|
|
||||||
title=event,
|
|
||||||
description=message,
|
|
||||||
icon=image
|
|
||||||
)
|
|
||||||
except gntp.notifier.errors.NetworkError:
|
|
||||||
logger.warning('Growl notification failed: network error')
|
|
||||||
return
|
|
||||||
|
|
||||||
logger.info("Growl notifications sent.")
|
|
||||||
|
|
||||||
def updateLibrary(self):
|
|
||||||
# For uniformity reasons not removed
|
|
||||||
return
|
|
||||||
|
|
||||||
def test(self, host, password):
|
|
||||||
self.enabled = True
|
|
||||||
self.host = host
|
|
||||||
self.password = password
|
|
||||||
|
|
||||||
self.notify('ZOMG Lazors Pewpewpew!', 'Test Message')
|
|
||||||
|
|
||||||
|
|
||||||
class PROWL(object):
|
class PROWL(object):
|
||||||
"""
|
"""
|
||||||
Prowl notifications.
|
Prowl notifications.
|
||||||
@@ -840,105 +752,12 @@ class TwitterNotifier(object):
|
|||||||
|
|
||||||
|
|
||||||
class OSX_NOTIFY(object):
|
class OSX_NOTIFY(object):
|
||||||
def __init__(self):
|
def notify(self, title, subtitle):
|
||||||
try:
|
try:
|
||||||
self.objc = __import__("objc")
|
script = f'display notification "{subtitle}" with title "{title}"'
|
||||||
self.AppKit = __import__("AppKit")
|
subprocess.run(["osascript", "-e", script])
|
||||||
except:
|
|
||||||
logger.warn('OS X Notification: Cannot import objc or AppKit')
|
|
||||||
pass
|
|
||||||
|
|
||||||
def swizzle(self, cls, SEL, func):
|
|
||||||
old_IMP = getattr(cls, SEL, None)
|
|
||||||
if old_IMP is None:
|
|
||||||
old_IMP = cls.instanceMethodForSelector_(SEL)
|
|
||||||
|
|
||||||
def wrapper(self, *args, **kwargs):
|
|
||||||
return func(self, old_IMP, *args, **kwargs)
|
|
||||||
|
|
||||||
new_IMP = self.objc.selector(
|
|
||||||
wrapper,
|
|
||||||
selector=old_IMP.selector,
|
|
||||||
signature=old_IMP.signature
|
|
||||||
)
|
|
||||||
self.objc.classAddMethod(cls, SEL.encode(), new_IMP)
|
|
||||||
|
|
||||||
def notify(self, title, subtitle=None, text=None, sound=True, image=None):
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.swizzle(
|
|
||||||
self.objc.lookUpClass('NSBundle'),
|
|
||||||
'bundleIdentifier',
|
|
||||||
self.swizzled_bundleIdentifier
|
|
||||||
)
|
|
||||||
|
|
||||||
NSUserNotification = self.objc.lookUpClass('NSUserNotification')
|
|
||||||
NSUserNotificationCenter = self.objc.lookUpClass(
|
|
||||||
'NSUserNotificationCenter')
|
|
||||||
NSAutoreleasePool = self.objc.lookUpClass('NSAutoreleasePool')
|
|
||||||
|
|
||||||
if not NSUserNotification or not NSUserNotificationCenter:
|
|
||||||
return False
|
|
||||||
|
|
||||||
pool = NSAutoreleasePool.alloc().init()
|
|
||||||
|
|
||||||
notification = NSUserNotification.alloc().init()
|
|
||||||
notification.setTitle_(title)
|
|
||||||
if subtitle:
|
|
||||||
notification.setSubtitle_(subtitle)
|
|
||||||
if text:
|
|
||||||
notification.setInformativeText_(text)
|
|
||||||
if sound:
|
|
||||||
notification.setSoundName_(
|
|
||||||
"NSUserNotificationDefaultSoundName")
|
|
||||||
if image:
|
|
||||||
source_img = self.AppKit.NSImage.alloc().\
|
|
||||||
initByReferencingFile_(image)
|
|
||||||
notification.setContentImage_(source_img)
|
|
||||||
# notification.set_identityImage_(source_img)
|
|
||||||
notification.setHasActionButton_(False)
|
|
||||||
|
|
||||||
notification_center = NSUserNotificationCenter.\
|
|
||||||
defaultUserNotificationCenter()
|
|
||||||
notification_center.deliverNotification_(notification)
|
|
||||||
|
|
||||||
del pool
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn('Error sending OS X Notification: %s' % e)
|
logger.warn(f"Error sending MacOS Notification: {e}")
|
||||||
return False
|
|
||||||
|
|
||||||
def swizzled_bundleIdentifier(self, original, swizzled):
|
|
||||||
return 'ade.headphones.osxnotify'
|
|
||||||
|
|
||||||
|
|
||||||
class BOXCAR(object):
|
|
||||||
def __init__(self):
|
|
||||||
self.url = 'https://new.boxcar.io/api/notifications'
|
|
||||||
|
|
||||||
def notify(self, title, message, rgid=None):
|
|
||||||
try:
|
|
||||||
if rgid:
|
|
||||||
message += '<br></br><a href="https://musicbrainz.org/' \
|
|
||||||
'release-group/%s">MusicBrainz</a>' % rgid
|
|
||||||
|
|
||||||
data = urllib.parse.urlencode({
|
|
||||||
'user_credentials': headphones.CONFIG.BOXCAR_TOKEN,
|
|
||||||
'notification[title]': title.encode('utf-8'),
|
|
||||||
'notification[long_message]': message.encode('utf-8'),
|
|
||||||
'notification[sound]': "done",
|
|
||||||
'notification[icon_url]': "https://raw.githubusercontent.com/rembo10/headphones/master/data/images"
|
|
||||||
"/headphoneslogo.png"
|
|
||||||
})
|
|
||||||
|
|
||||||
req = urllib.request.Request(self.url)
|
|
||||||
handle = urllib.request.urlopen(req, data)
|
|
||||||
handle.close()
|
|
||||||
return True
|
|
||||||
|
|
||||||
except urllib.error.URLError as e:
|
|
||||||
logger.warn('Error sending Boxcar2 Notification: %s' % e)
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -550,11 +550,6 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
|
|||||||
pushmessage = release['ArtistName'] + ' - ' + release['AlbumTitle']
|
pushmessage = release['ArtistName'] + ' - ' + release['AlbumTitle']
|
||||||
statusmessage = "Download and Postprocessing completed"
|
statusmessage = "Download and Postprocessing completed"
|
||||||
|
|
||||||
if headphones.CONFIG.GROWL_ENABLED:
|
|
||||||
logger.info("Growl request")
|
|
||||||
growl = notifiers.GROWL()
|
|
||||||
growl.notify(pushmessage, statusmessage)
|
|
||||||
|
|
||||||
if headphones.CONFIG.PROWL_ENABLED:
|
if headphones.CONFIG.PROWL_ENABLED:
|
||||||
logger.info("Prowl request")
|
logger.info("Prowl request")
|
||||||
prowl = notifiers.PROWL()
|
prowl = notifiers.PROWL()
|
||||||
@@ -623,21 +618,9 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
|
|||||||
#twitter.notify_download(pushmessage)
|
#twitter.notify_download(pushmessage)
|
||||||
|
|
||||||
if headphones.CONFIG.OSX_NOTIFY_ENABLED:
|
if headphones.CONFIG.OSX_NOTIFY_ENABLED:
|
||||||
from headphones import cache
|
logger.info("Sending MacOS notification")
|
||||||
c = cache.Cache()
|
osx = notifiers.OSX_NOTIFY()
|
||||||
album_art = c.get_artwork_from_cache(None, release['AlbumID'])
|
osx.notify(f"Headphones Processed", f"{pushmessage}\n{statusmessage}")
|
||||||
logger.info("Sending OS X notification")
|
|
||||||
osx_notify = notifiers.OSX_NOTIFY()
|
|
||||||
osx_notify.notify(release['ArtistName'],
|
|
||||||
release['AlbumTitle'],
|
|
||||||
statusmessage,
|
|
||||||
image=album_art)
|
|
||||||
|
|
||||||
if headphones.CONFIG.BOXCAR_ENABLED:
|
|
||||||
logger.info("Sending Boxcar2 notification")
|
|
||||||
boxcar = notifiers.BOXCAR()
|
|
||||||
boxcar.notify('Headphones processed: ' + pushmessage,
|
|
||||||
statusmessage, release['AlbumID'])
|
|
||||||
|
|
||||||
if headphones.CONFIG.SUBSONIC_ENABLED:
|
if headphones.CONFIG.SUBSONIC_ENABLED:
|
||||||
logger.info("Sending Subsonic update")
|
logger.info("Sending Subsonic update")
|
||||||
|
|||||||
+23
-34
@@ -127,24 +127,24 @@ def read_torrent_name(torrent_file, default_name=None):
|
|||||||
with open(torrent_file, "rb") as fp:
|
with open(torrent_file, "rb") as fp:
|
||||||
torrent_info = bdecode(fp.read())
|
torrent_info = bdecode(fp.read())
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
logger.error("Unable to open torrent file: %s", torrent_file)
|
logger.error("Unable to open torrent file: %s. %s", torrent_file, e)
|
||||||
return
|
|
||||||
|
|
||||||
# Read dictionary
|
|
||||||
if torrent_info:
|
|
||||||
try:
|
|
||||||
return torrent_info["info"]["name"]
|
|
||||||
except KeyError:
|
|
||||||
if default_name:
|
|
||||||
logger.warning("Couldn't get name from torrent file: %s. "
|
|
||||||
"Defaulting to '%s'", e, default_name)
|
|
||||||
else:
|
|
||||||
logger.warning("Couldn't get name from torrent file: %s. No "
|
|
||||||
"default given", e)
|
|
||||||
|
|
||||||
# Return default
|
|
||||||
return default_name
|
return default_name
|
||||||
|
|
||||||
|
name = None
|
||||||
|
info = torrent_info.get(b'info')
|
||||||
|
if info:
|
||||||
|
raw_name = info.get(b'name')
|
||||||
|
if raw_name:
|
||||||
|
name = raw_name.decode(headphones.SYS_ENCODING, 'replace')
|
||||||
|
|
||||||
|
if not name:
|
||||||
|
if default_name:
|
||||||
|
logger.warning("Couldn't get name from torrent file: %s. Defaulting to '%s'", torrent_file, default_name)
|
||||||
|
else:
|
||||||
|
logger.warning("Couldn't get name from torrent file: %s. No default given", torrent_file)
|
||||||
|
|
||||||
|
return name or default_name
|
||||||
|
|
||||||
|
|
||||||
def calculate_torrent_hash(link, data=None):
|
def calculate_torrent_hash(link, data=None):
|
||||||
"""
|
"""
|
||||||
@@ -991,6 +991,8 @@ def send_to_downloader(data, result, album):
|
|||||||
folder_name = read_torrent_name(
|
folder_name = read_torrent_name(
|
||||||
download_path,
|
download_path,
|
||||||
result.title)
|
result.title)
|
||||||
|
if folder_name:
|
||||||
|
logger.info('Torrent folder name: %s' % folder_name)
|
||||||
|
|
||||||
# Break for loop
|
# Break for loop
|
||||||
break
|
break
|
||||||
@@ -1181,10 +1183,6 @@ def send_to_downloader(data, result, album):
|
|||||||
provider = get_provider_name(result.provider)
|
provider = get_provider_name(result.provider)
|
||||||
name = folder_name if folder_name else None
|
name = folder_name if folder_name else None
|
||||||
|
|
||||||
if headphones.CONFIG.GROWL_ENABLED and headphones.CONFIG.GROWL_ONSNATCH:
|
|
||||||
logger.info("Sending Growl notification")
|
|
||||||
growl = notifiers.GROWL()
|
|
||||||
growl.notify(name, "Download started")
|
|
||||||
if headphones.CONFIG.PROWL_ENABLED and headphones.CONFIG.PROWL_ONSNATCH:
|
if headphones.CONFIG.PROWL_ENABLED and headphones.CONFIG.PROWL_ONSNATCH:
|
||||||
logger.info("Sending Prowl notification")
|
logger.info("Sending Prowl notification")
|
||||||
prowl = notifiers.PROWL()
|
prowl = notifiers.PROWL()
|
||||||
@@ -1226,21 +1224,12 @@ def send_to_downloader(data, result, album):
|
|||||||
logger.info("Sending Pushalot notification")
|
logger.info("Sending Pushalot notification")
|
||||||
pushalot = notifiers.PUSHALOT()
|
pushalot = notifiers.PUSHALOT()
|
||||||
pushalot.notify(name, "Download started")
|
pushalot.notify(name, "Download started")
|
||||||
|
|
||||||
if headphones.CONFIG.OSX_NOTIFY_ENABLED and headphones.CONFIG.OSX_NOTIFY_ONSNATCH:
|
if headphones.CONFIG.OSX_NOTIFY_ENABLED and headphones.CONFIG.OSX_NOTIFY_ONSNATCH:
|
||||||
from headphones import cache
|
logger.info("Sending MacOS notification")
|
||||||
c = cache.Cache()
|
osx = notifiers.OSX_NOTIFY()
|
||||||
album_art = c.get_artwork_from_cache(None, rgid)
|
osx.notify(f"Headphones Snatched", f"{artist} - {albumname}\nFrom {provider}, {name}")
|
||||||
logger.info("Sending OS X notification")
|
|
||||||
osx_notify = notifiers.OSX_NOTIFY()
|
|
||||||
osx_notify.notify(artist,
|
|
||||||
albumname,
|
|
||||||
'Snatched: ' + provider + '. ' + name,
|
|
||||||
image=album_art)
|
|
||||||
if headphones.CONFIG.BOXCAR_ENABLED and headphones.CONFIG.BOXCAR_ONSNATCH:
|
|
||||||
logger.info("Sending Boxcar2 notification")
|
|
||||||
b2msg = 'From ' + provider + '<br></br>' + name
|
|
||||||
boxcar = notifiers.BOXCAR()
|
|
||||||
boxcar.notify('Headphones snatched: ' + title, b2msg, rgid)
|
|
||||||
if headphones.CONFIG.EMAIL_ENABLED and headphones.CONFIG.EMAIL_ONSNATCH:
|
if headphones.CONFIG.EMAIL_ENABLED and headphones.CONFIG.EMAIL_ONSNATCH:
|
||||||
logger.info("Sending Email notification")
|
logger.info("Sending Email notification")
|
||||||
email = notifiers.Email()
|
email = notifiers.Email()
|
||||||
|
|||||||
+4
-26
@@ -1316,10 +1316,6 @@ class WebInterface(object):
|
|||||||
"encoder_multicore": checked(headphones.CONFIG.ENCODER_MULTICORE),
|
"encoder_multicore": checked(headphones.CONFIG.ENCODER_MULTICORE),
|
||||||
"encoder_multicore_count": int(headphones.CONFIG.ENCODER_MULTICORE_COUNT),
|
"encoder_multicore_count": int(headphones.CONFIG.ENCODER_MULTICORE_COUNT),
|
||||||
"delete_lossless_files": checked(headphones.CONFIG.DELETE_LOSSLESS_FILES),
|
"delete_lossless_files": checked(headphones.CONFIG.DELETE_LOSSLESS_FILES),
|
||||||
"growl_enabled": checked(headphones.CONFIG.GROWL_ENABLED),
|
|
||||||
"growl_onsnatch": checked(headphones.CONFIG.GROWL_ONSNATCH),
|
|
||||||
"growl_host": headphones.CONFIG.GROWL_HOST,
|
|
||||||
"growl_password": headphones.CONFIG.GROWL_PASSWORD,
|
|
||||||
"prowl_enabled": checked(headphones.CONFIG.PROWL_ENABLED),
|
"prowl_enabled": checked(headphones.CONFIG.PROWL_ENABLED),
|
||||||
"prowl_onsnatch": checked(headphones.CONFIG.PROWL_ONSNATCH),
|
"prowl_onsnatch": checked(headphones.CONFIG.PROWL_ONSNATCH),
|
||||||
"prowl_keys": headphones.CONFIG.PROWL_KEYS,
|
"prowl_keys": headphones.CONFIG.PROWL_KEYS,
|
||||||
@@ -1369,10 +1365,6 @@ class WebInterface(object):
|
|||||||
"twitter_onsnatch": checked(headphones.CONFIG.TWITTER_ONSNATCH),
|
"twitter_onsnatch": checked(headphones.CONFIG.TWITTER_ONSNATCH),
|
||||||
"osx_notify_enabled": checked(headphones.CONFIG.OSX_NOTIFY_ENABLED),
|
"osx_notify_enabled": checked(headphones.CONFIG.OSX_NOTIFY_ENABLED),
|
||||||
"osx_notify_onsnatch": checked(headphones.CONFIG.OSX_NOTIFY_ONSNATCH),
|
"osx_notify_onsnatch": checked(headphones.CONFIG.OSX_NOTIFY_ONSNATCH),
|
||||||
"osx_notify_app": headphones.CONFIG.OSX_NOTIFY_APP,
|
|
||||||
"boxcar_enabled": checked(headphones.CONFIG.BOXCAR_ENABLED),
|
|
||||||
"boxcar_onsnatch": checked(headphones.CONFIG.BOXCAR_ONSNATCH),
|
|
||||||
"boxcar_token": headphones.CONFIG.BOXCAR_TOKEN,
|
|
||||||
"mirrorlist": headphones.MIRRORLIST,
|
"mirrorlist": headphones.MIRRORLIST,
|
||||||
"mirror": headphones.CONFIG.MIRROR,
|
"mirror": headphones.CONFIG.MIRROR,
|
||||||
"customhost": headphones.CONFIG.CUSTOMHOST,
|
"customhost": headphones.CONFIG.CUSTOMHOST,
|
||||||
@@ -1471,16 +1463,16 @@ class WebInterface(object):
|
|||||||
"wait_until_release_date", "autowant_upcoming", "autowant_all",
|
"wait_until_release_date", "autowant_upcoming", "autowant_all",
|
||||||
"autowant_manually_added", "do_not_process_unmatched", "keep_torrent_files",
|
"autowant_manually_added", "do_not_process_unmatched", "keep_torrent_files",
|
||||||
"music_encoder", "mb_ignore_age_missing",
|
"music_encoder", "mb_ignore_age_missing",
|
||||||
"encoderlossless", "encoder_multicore", "delete_lossless_files", "growl_enabled",
|
"encoderlossless", "encoder_multicore", "delete_lossless_files",
|
||||||
"growl_onsnatch", "prowl_enabled",
|
"prowl_enabled", "prowl_onsnatch",
|
||||||
"prowl_onsnatch", "xbmc_enabled", "xbmc_update", "xbmc_notify", "lms_enabled",
|
"xbmc_enabled", "xbmc_update", "xbmc_notify", "lms_enabled",
|
||||||
"plex_enabled", "plex_update", "plex_notify",
|
"plex_enabled", "plex_update", "plex_notify",
|
||||||
"nma_enabled", "nma_onsnatch", "pushalot_enabled", "pushalot_onsnatch",
|
"nma_enabled", "nma_onsnatch", "pushalot_enabled", "pushalot_onsnatch",
|
||||||
"synoindex_enabled", "pushover_enabled",
|
"synoindex_enabled", "pushover_enabled",
|
||||||
"pushover_onsnatch", "pushbullet_enabled", "pushbullet_onsnatch", "subsonic_enabled",
|
"pushover_onsnatch", "pushbullet_enabled", "pushbullet_onsnatch", "subsonic_enabled",
|
||||||
"twitter_enabled", "twitter_onsnatch",
|
"twitter_enabled", "twitter_onsnatch",
|
||||||
"telegram_enabled", "telegram_onsnatch",
|
"telegram_enabled", "telegram_onsnatch",
|
||||||
"osx_notify_enabled", "osx_notify_onsnatch", "boxcar_enabled", "boxcar_onsnatch",
|
"osx_notify_enabled", "osx_notify_onsnatch",
|
||||||
"songkick_enabled", "songkick_filter_enabled",
|
"songkick_enabled", "songkick_filter_enabled",
|
||||||
"mpc_enabled", "email_enabled", "email_ssl", "email_tls", "email_onsnatch",
|
"mpc_enabled", "email_enabled", "email_ssl", "email_tls", "email_onsnatch",
|
||||||
"customauth", "idtag", "deluge_paused",
|
"customauth", "idtag", "deluge_paused",
|
||||||
@@ -1718,20 +1710,6 @@ class WebInterface(object):
|
|||||||
else:
|
else:
|
||||||
return "Error sending tweet"
|
return "Error sending tweet"
|
||||||
|
|
||||||
@cherrypy.expose
|
|
||||||
def osxnotifyregister(self, app):
|
|
||||||
cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store"
|
|
||||||
from osxnotify import registerapp as osxnotify
|
|
||||||
result, msg = osxnotify.registerapp(app)
|
|
||||||
if result:
|
|
||||||
osx_notify = notifiers.OSX_NOTIFY()
|
|
||||||
osx_notify.notify('Registered', result, 'Success :-)')
|
|
||||||
logger.info(
|
|
||||||
'Registered %s, to re-register a different app, delete this app first' % result)
|
|
||||||
else:
|
|
||||||
logger.warn(msg)
|
|
||||||
return msg
|
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
def testPushover(self):
|
def testPushover(self):
|
||||||
logger.info("Sending Pushover notification")
|
logger.info("Sending Pushover notification")
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
Copyright (c) 2013 Paul Traylor
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining
|
|
||||||
a copy of this software and associated documentation files (the
|
|
||||||
"Software"), to deal in the Software without restriction, including
|
|
||||||
without limitation the rights to use, copy, modify, merge, publish,
|
|
||||||
distribute, sublicense, and/or sell copies of the Software, and to
|
|
||||||
permit persons to whom the Software is furnished to do so, subject to
|
|
||||||
the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be
|
|
||||||
included in all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
||||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
||||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
||||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
-141
@@ -1,141 +0,0 @@
|
|||||||
# Copyright: 2013 Paul Traylor
|
|
||||||
# These sources are released under the terms of the MIT license: see LICENSE
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from optparse import OptionParser, OptionGroup
|
|
||||||
|
|
||||||
from gntp.notifier import GrowlNotifier
|
|
||||||
from gntp.shim import RawConfigParser
|
|
||||||
from gntp.version import __version__
|
|
||||||
|
|
||||||
DEFAULT_CONFIG = os.path.expanduser('~/.gntp')
|
|
||||||
|
|
||||||
config = RawConfigParser({
|
|
||||||
'hostname': 'localhost',
|
|
||||||
'password': None,
|
|
||||||
'port': 23053,
|
|
||||||
})
|
|
||||||
config.read([DEFAULT_CONFIG])
|
|
||||||
if not config.has_section('gntp'):
|
|
||||||
config.add_section('gntp')
|
|
||||||
|
|
||||||
|
|
||||||
class ClientParser(OptionParser):
|
|
||||||
def __init__(self):
|
|
||||||
OptionParser.__init__(self, version="%%prog %s" % __version__)
|
|
||||||
|
|
||||||
group = OptionGroup(self, "Network Options")
|
|
||||||
group.add_option("-H", "--host",
|
|
||||||
dest="host", default=config.get('gntp', 'hostname'),
|
|
||||||
help="Specify a hostname to which to send a remote notification. [%default]")
|
|
||||||
group.add_option("--port",
|
|
||||||
dest="port", default=config.getint('gntp', 'port'), type="int",
|
|
||||||
help="port to listen on [%default]")
|
|
||||||
group.add_option("-P", "--password",
|
|
||||||
dest='password', default=config.get('gntp', 'password'),
|
|
||||||
help="Network password")
|
|
||||||
self.add_option_group(group)
|
|
||||||
|
|
||||||
group = OptionGroup(self, "Notification Options")
|
|
||||||
group.add_option("-n", "--name",
|
|
||||||
dest="app", default='Python GNTP Test Client',
|
|
||||||
help="Set the name of the application [%default]")
|
|
||||||
group.add_option("-s", "--sticky",
|
|
||||||
dest='sticky', default=False, action="store_true",
|
|
||||||
help="Make the notification sticky [%default]")
|
|
||||||
group.add_option("--image",
|
|
||||||
dest="icon", default=None,
|
|
||||||
help="Icon for notification (URL or /path/to/file)")
|
|
||||||
group.add_option("-m", "--message",
|
|
||||||
dest="message", default=None,
|
|
||||||
help="Sets the message instead of using stdin")
|
|
||||||
group.add_option("-p", "--priority",
|
|
||||||
dest="priority", default=0, type="int",
|
|
||||||
help="-2 to 2 [%default]")
|
|
||||||
group.add_option("-d", "--identifier",
|
|
||||||
dest="identifier",
|
|
||||||
help="Identifier for coalescing")
|
|
||||||
group.add_option("-t", "--title",
|
|
||||||
dest="title", default=None,
|
|
||||||
help="Set the title of the notification [%default]")
|
|
||||||
group.add_option("-N", "--notification",
|
|
||||||
dest="name", default='Notification',
|
|
||||||
help="Set the notification name [%default]")
|
|
||||||
group.add_option("--callback",
|
|
||||||
dest="callback",
|
|
||||||
help="URL callback")
|
|
||||||
self.add_option_group(group)
|
|
||||||
|
|
||||||
# Extra Options
|
|
||||||
self.add_option('-v', '--verbose',
|
|
||||||
dest='verbose', default=0, action='count',
|
|
||||||
help="Verbosity levels")
|
|
||||||
|
|
||||||
def parse_args(self, args=None, values=None):
|
|
||||||
values, args = OptionParser.parse_args(self, args, values)
|
|
||||||
|
|
||||||
if values.message is None:
|
|
||||||
print('Enter a message followed by Ctrl-D')
|
|
||||||
try:
|
|
||||||
message = sys.stdin.read()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
exit()
|
|
||||||
else:
|
|
||||||
message = values.message
|
|
||||||
|
|
||||||
if values.title is None:
|
|
||||||
values.title = ' '.join(args)
|
|
||||||
|
|
||||||
# If we still have an empty title, use the
|
|
||||||
# first bit of the message as the title
|
|
||||||
if values.title == '':
|
|
||||||
values.title = message[:20]
|
|
||||||
|
|
||||||
values.verbose = logging.WARNING - values.verbose * 10
|
|
||||||
|
|
||||||
return values, message
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
(options, message) = ClientParser().parse_args()
|
|
||||||
logging.basicConfig(level=options.verbose)
|
|
||||||
if not os.path.exists(DEFAULT_CONFIG):
|
|
||||||
logging.info('No config read found at %s', DEFAULT_CONFIG)
|
|
||||||
|
|
||||||
growl = GrowlNotifier(
|
|
||||||
applicationName=options.app,
|
|
||||||
notifications=[options.name],
|
|
||||||
defaultNotifications=[options.name],
|
|
||||||
hostname=options.host,
|
|
||||||
password=options.password,
|
|
||||||
port=options.port,
|
|
||||||
)
|
|
||||||
result = growl.register()
|
|
||||||
if result is not True:
|
|
||||||
exit(result)
|
|
||||||
|
|
||||||
# This would likely be better placed within the growl notifier
|
|
||||||
# class but until I make _checkIcon smarter this is "easier"
|
|
||||||
if options.icon is not None and not options.icon.startswith('http'):
|
|
||||||
logging.info('Loading image %s', options.icon)
|
|
||||||
f = open(options.icon)
|
|
||||||
options.icon = f.read()
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
result = growl.notify(
|
|
||||||
noteType=options.name,
|
|
||||||
title=options.title,
|
|
||||||
description=message,
|
|
||||||
icon=options.icon,
|
|
||||||
sticky=options.sticky,
|
|
||||||
priority=options.priority,
|
|
||||||
callback=options.callback,
|
|
||||||
identifier=options.identifier,
|
|
||||||
)
|
|
||||||
if result is not True:
|
|
||||||
exit(result)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
# Copyright: 2013 Paul Traylor
|
|
||||||
# These sources are released under the terms of the MIT license: see LICENSE
|
|
||||||
|
|
||||||
"""
|
|
||||||
The gntp.config module is provided as an extended GrowlNotifier object that takes
|
|
||||||
advantage of the ConfigParser module to allow us to setup some default values
|
|
||||||
(such as hostname, password, and port) in a more global way to be shared among
|
|
||||||
programs using gntp
|
|
||||||
"""
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
|
|
||||||
import gntp.notifier
|
|
||||||
import gntp.shim
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'mini',
|
|
||||||
'GrowlNotifier'
|
|
||||||
]
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class GrowlNotifier(gntp.notifier.GrowlNotifier):
|
|
||||||
"""
|
|
||||||
ConfigParser enhanced GrowlNotifier object
|
|
||||||
|
|
||||||
For right now, we are only interested in letting users overide certain
|
|
||||||
values from ~/.gntp
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
[gntp]
|
|
||||||
hostname = ?
|
|
||||||
password = ?
|
|
||||||
port = ?
|
|
||||||
"""
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
config = gntp.shim.RawConfigParser({
|
|
||||||
'hostname': kwargs.get('hostname', 'localhost'),
|
|
||||||
'password': kwargs.get('password'),
|
|
||||||
'port': kwargs.get('port', 23053),
|
|
||||||
})
|
|
||||||
|
|
||||||
config.read([os.path.expanduser('~/.gntp')])
|
|
||||||
|
|
||||||
# If the file does not exist, then there will be no gntp section defined
|
|
||||||
# and the config.get() lines below will get confused. Since we are not
|
|
||||||
# saving the config, it should be safe to just add it here so the
|
|
||||||
# code below doesn't complain
|
|
||||||
if not config.has_section('gntp'):
|
|
||||||
logger.info('Error reading ~/.gntp config file')
|
|
||||||
config.add_section('gntp')
|
|
||||||
|
|
||||||
kwargs['password'] = config.get('gntp', 'password')
|
|
||||||
kwargs['hostname'] = config.get('gntp', 'hostname')
|
|
||||||
kwargs['port'] = config.getint('gntp', 'port')
|
|
||||||
|
|
||||||
super(GrowlNotifier, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def mini(description, **kwargs):
|
|
||||||
"""Single notification function
|
|
||||||
|
|
||||||
Simple notification function in one line. Has only one required parameter
|
|
||||||
and attempts to use reasonable defaults for everything else
|
|
||||||
:param string description: Notification message
|
|
||||||
"""
|
|
||||||
kwargs['notifierFactory'] = GrowlNotifier
|
|
||||||
gntp.notifier.mini(description, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
# If we're running this module directly we're likely running it as a test
|
|
||||||
# so extra debugging is useful
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
|
||||||
mini('Testing mini notification')
|
|
||||||
@@ -1,511 +0,0 @@
|
|||||||
# Copyright: 2013 Paul Traylor
|
|
||||||
# These sources are released under the terms of the MIT license: see LICENSE
|
|
||||||
|
|
||||||
import hashlib
|
|
||||||
import re
|
|
||||||
import time
|
|
||||||
|
|
||||||
import gntp.shim
|
|
||||||
import gntp.errors as errors
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'GNTPRegister',
|
|
||||||
'GNTPNotice',
|
|
||||||
'GNTPSubscribe',
|
|
||||||
'GNTPOK',
|
|
||||||
'GNTPError',
|
|
||||||
'parse_gntp',
|
|
||||||
]
|
|
||||||
|
|
||||||
#GNTP/<version> <messagetype> <encryptionAlgorithmID>[:<ivValue>][ <keyHashAlgorithmID>:<keyHash>.<salt>]
|
|
||||||
GNTP_INFO_LINE = re.compile(
|
|
||||||
'GNTP/(?P<version>\d+\.\d+) (?P<messagetype>REGISTER|NOTIFY|SUBSCRIBE|\-OK|\-ERROR)' +
|
|
||||||
' (?P<encryptionAlgorithmID>[A-Z0-9]+(:(?P<ivValue>[A-F0-9]+))?) ?' +
|
|
||||||
'((?P<keyHashAlgorithmID>[A-Z0-9]+):(?P<keyHash>[A-F0-9]+).(?P<salt>[A-F0-9]+))?\r\n',
|
|
||||||
re.IGNORECASE
|
|
||||||
)
|
|
||||||
|
|
||||||
GNTP_INFO_LINE_SHORT = re.compile(
|
|
||||||
'GNTP/(?P<version>\d+\.\d+) (?P<messagetype>REGISTER|NOTIFY|SUBSCRIBE|\-OK|\-ERROR)',
|
|
||||||
re.IGNORECASE
|
|
||||||
)
|
|
||||||
|
|
||||||
GNTP_HEADER = re.compile('([\w-]+):(.+)')
|
|
||||||
|
|
||||||
GNTP_EOL = gntp.shim.b('\r\n')
|
|
||||||
GNTP_SEP = gntp.shim.b(': ')
|
|
||||||
|
|
||||||
|
|
||||||
class _GNTPBuffer(gntp.shim.StringIO):
|
|
||||||
"""GNTP Buffer class"""
|
|
||||||
def writeln(self, value=None):
|
|
||||||
if value:
|
|
||||||
self.write(gntp.shim.b(value))
|
|
||||||
self.write(GNTP_EOL)
|
|
||||||
|
|
||||||
def writeheader(self, key, value):
|
|
||||||
if not isinstance(value, str):
|
|
||||||
value = str(value)
|
|
||||||
self.write(gntp.shim.b(key))
|
|
||||||
self.write(GNTP_SEP)
|
|
||||||
self.write(gntp.shim.b(value))
|
|
||||||
self.write(GNTP_EOL)
|
|
||||||
|
|
||||||
|
|
||||||
class _GNTPBase(object):
|
|
||||||
"""Base initilization
|
|
||||||
|
|
||||||
:param string messagetype: GNTP Message type
|
|
||||||
:param string version: GNTP Protocol version
|
|
||||||
:param string encription: Encryption protocol
|
|
||||||
"""
|
|
||||||
def __init__(self, messagetype=None, version='1.0', encryption=None):
|
|
||||||
self.info = {
|
|
||||||
'version': version,
|
|
||||||
'messagetype': messagetype,
|
|
||||||
'encryptionAlgorithmID': encryption
|
|
||||||
}
|
|
||||||
self.hash_algo = {
|
|
||||||
'MD5': hashlib.md5,
|
|
||||||
'SHA1': hashlib.sha1,
|
|
||||||
'SHA256': hashlib.sha256,
|
|
||||||
'SHA512': hashlib.sha512,
|
|
||||||
}
|
|
||||||
self.headers = {}
|
|
||||||
self.resources = {}
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.encode()
|
|
||||||
|
|
||||||
def _parse_info(self, data):
|
|
||||||
"""Parse the first line of a GNTP message to get security and other info values
|
|
||||||
|
|
||||||
:param string data: GNTP Message
|
|
||||||
:return dict: Parsed GNTP Info line
|
|
||||||
"""
|
|
||||||
|
|
||||||
match = GNTP_INFO_LINE.match(data)
|
|
||||||
|
|
||||||
if not match:
|
|
||||||
raise errors.ParseError('ERROR_PARSING_INFO_LINE')
|
|
||||||
|
|
||||||
info = match.groupdict()
|
|
||||||
if info['encryptionAlgorithmID'] == 'NONE':
|
|
||||||
info['encryptionAlgorithmID'] = None
|
|
||||||
|
|
||||||
return info
|
|
||||||
|
|
||||||
def set_password(self, password, encryptAlgo='MD5'):
|
|
||||||
"""Set a password for a GNTP Message
|
|
||||||
|
|
||||||
:param string password: Null to clear password
|
|
||||||
:param string encryptAlgo: Supports MD5, SHA1, SHA256, SHA512
|
|
||||||
"""
|
|
||||||
if not password:
|
|
||||||
self.info['encryptionAlgorithmID'] = None
|
|
||||||
self.info['keyHashAlgorithm'] = None
|
|
||||||
return
|
|
||||||
|
|
||||||
self.password = gntp.shim.b(password)
|
|
||||||
self.encryptAlgo = encryptAlgo.upper()
|
|
||||||
|
|
||||||
if not self.encryptAlgo in self.hash_algo:
|
|
||||||
raise errors.UnsupportedError('INVALID HASH "%s"' % self.encryptAlgo)
|
|
||||||
|
|
||||||
hashfunction = self.hash_algo.get(self.encryptAlgo)
|
|
||||||
|
|
||||||
password = password.encode('utf8')
|
|
||||||
seed = time.ctime().encode('utf8')
|
|
||||||
salt = hashfunction(seed).hexdigest()
|
|
||||||
saltHash = hashfunction(seed).digest()
|
|
||||||
keyBasis = password + saltHash
|
|
||||||
key = hashfunction(keyBasis).digest()
|
|
||||||
keyHash = hashfunction(key).hexdigest()
|
|
||||||
|
|
||||||
self.info['keyHashAlgorithmID'] = self.encryptAlgo
|
|
||||||
self.info['keyHash'] = keyHash.upper()
|
|
||||||
self.info['salt'] = salt.upper()
|
|
||||||
|
|
||||||
def _decode_hex(self, value):
|
|
||||||
"""Helper function to decode hex string to `proper` hex string
|
|
||||||
|
|
||||||
:param string value: Human readable hex string
|
|
||||||
:return string: Hex string
|
|
||||||
"""
|
|
||||||
result = ''
|
|
||||||
for i in range(0, len(value), 2):
|
|
||||||
tmp = int(value[i:i + 2], 16)
|
|
||||||
result += chr(tmp)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _decode_binary(self, rawIdentifier, identifier):
|
|
||||||
rawIdentifier += '\r\n\r\n'
|
|
||||||
dataLength = int(identifier['Length'])
|
|
||||||
pointerStart = self.raw.find(rawIdentifier) + len(rawIdentifier)
|
|
||||||
pointerEnd = pointerStart + dataLength
|
|
||||||
data = self.raw[pointerStart:pointerEnd]
|
|
||||||
if not len(data) == dataLength:
|
|
||||||
raise errors.ParseError('INVALID_DATA_LENGTH Expected: %s Received %s' % (dataLength, len(data)))
|
|
||||||
return data
|
|
||||||
|
|
||||||
def _validate_password(self, password):
|
|
||||||
"""Validate GNTP Message against stored password"""
|
|
||||||
self.password = password
|
|
||||||
if password is None:
|
|
||||||
raise errors.AuthError('Missing password')
|
|
||||||
keyHash = self.info.get('keyHash', None)
|
|
||||||
if keyHash is None and self.password is None:
|
|
||||||
return True
|
|
||||||
if keyHash is None:
|
|
||||||
raise errors.AuthError('Invalid keyHash')
|
|
||||||
if self.password is None:
|
|
||||||
raise errors.AuthError('Missing password')
|
|
||||||
|
|
||||||
keyHashAlgorithmID = self.info.get('keyHashAlgorithmID','MD5')
|
|
||||||
|
|
||||||
password = self.password.encode('utf8')
|
|
||||||
saltHash = self._decode_hex(self.info['salt'])
|
|
||||||
|
|
||||||
keyBasis = password + saltHash
|
|
||||||
self.key = self.hash_algo[keyHashAlgorithmID](keyBasis).digest()
|
|
||||||
keyHash = self.hash_algo[keyHashAlgorithmID](self.key).hexdigest()
|
|
||||||
|
|
||||||
if not keyHash.upper() == self.info['keyHash'].upper():
|
|
||||||
raise errors.AuthError('Invalid Hash')
|
|
||||||
return True
|
|
||||||
|
|
||||||
def validate(self):
|
|
||||||
"""Verify required headers"""
|
|
||||||
for header in self._requiredHeaders:
|
|
||||||
if not self.headers.get(header, False):
|
|
||||||
raise errors.ParseError('Missing Notification Header: ' + header)
|
|
||||||
|
|
||||||
def _format_info(self):
|
|
||||||
"""Generate info line for GNTP Message
|
|
||||||
|
|
||||||
:return string:
|
|
||||||
"""
|
|
||||||
info = 'GNTP/%s %s' % (
|
|
||||||
self.info.get('version'),
|
|
||||||
self.info.get('messagetype'),
|
|
||||||
)
|
|
||||||
if self.info.get('encryptionAlgorithmID', None):
|
|
||||||
info += ' %s:%s' % (
|
|
||||||
self.info.get('encryptionAlgorithmID'),
|
|
||||||
self.info.get('ivValue'),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
info += ' NONE'
|
|
||||||
|
|
||||||
if self.info.get('keyHashAlgorithmID', None):
|
|
||||||
info += ' %s:%s.%s' % (
|
|
||||||
self.info.get('keyHashAlgorithmID'),
|
|
||||||
self.info.get('keyHash'),
|
|
||||||
self.info.get('salt')
|
|
||||||
)
|
|
||||||
|
|
||||||
return info
|
|
||||||
|
|
||||||
def _parse_dict(self, data):
|
|
||||||
"""Helper function to parse blocks of GNTP headers into a dictionary
|
|
||||||
|
|
||||||
:param string data:
|
|
||||||
:return dict: Dictionary of parsed GNTP Headers
|
|
||||||
"""
|
|
||||||
d = {}
|
|
||||||
for line in data.split('\r\n'):
|
|
||||||
match = GNTP_HEADER.match(line)
|
|
||||||
if not match:
|
|
||||||
continue
|
|
||||||
|
|
||||||
key = match.group(1).strip()
|
|
||||||
val = match.group(2).strip()
|
|
||||||
d[key] = val
|
|
||||||
return d
|
|
||||||
|
|
||||||
def add_header(self, key, value):
|
|
||||||
self.headers[key] = value
|
|
||||||
|
|
||||||
def add_resource(self, data):
|
|
||||||
"""Add binary resource
|
|
||||||
|
|
||||||
:param string data: Binary Data
|
|
||||||
"""
|
|
||||||
data = gntp.shim.b(data)
|
|
||||||
identifier = hashlib.md5(data).hexdigest()
|
|
||||||
self.resources[identifier] = data
|
|
||||||
return 'x-growl-resource://%s' % identifier
|
|
||||||
|
|
||||||
def decode(self, data, password=None):
|
|
||||||
"""Decode GNTP Message
|
|
||||||
|
|
||||||
:param string data:
|
|
||||||
"""
|
|
||||||
self.password = password
|
|
||||||
self.raw = gntp.shim.u(data)
|
|
||||||
parts = self.raw.split('\r\n\r\n')
|
|
||||||
self.info = self._parse_info(self.raw)
|
|
||||||
self.headers = self._parse_dict(parts[0])
|
|
||||||
|
|
||||||
def encode(self):
|
|
||||||
"""Encode a generic GNTP Message
|
|
||||||
|
|
||||||
:return string: GNTP Message ready to be sent. Returned as a byte string
|
|
||||||
"""
|
|
||||||
|
|
||||||
buff = _GNTPBuffer()
|
|
||||||
|
|
||||||
buff.writeln(self._format_info())
|
|
||||||
|
|
||||||
#Headers
|
|
||||||
for k, v in list(self.headers.items()):
|
|
||||||
buff.writeheader(k, v)
|
|
||||||
buff.writeln()
|
|
||||||
|
|
||||||
#Resources
|
|
||||||
for resource, data in list(self.resources.items()):
|
|
||||||
buff.writeheader('Identifier', resource)
|
|
||||||
buff.writeheader('Length', len(data))
|
|
||||||
buff.writeln()
|
|
||||||
buff.write(data)
|
|
||||||
buff.writeln()
|
|
||||||
buff.writeln()
|
|
||||||
|
|
||||||
return buff.getvalue()
|
|
||||||
|
|
||||||
|
|
||||||
class GNTPRegister(_GNTPBase):
|
|
||||||
"""Represents a GNTP Registration Command
|
|
||||||
|
|
||||||
:param string data: (Optional) See decode()
|
|
||||||
:param string password: (Optional) Password to use while encoding/decoding messages
|
|
||||||
"""
|
|
||||||
_requiredHeaders = [
|
|
||||||
'Application-Name',
|
|
||||||
'Notifications-Count'
|
|
||||||
]
|
|
||||||
_requiredNotificationHeaders = ['Notification-Name']
|
|
||||||
|
|
||||||
def __init__(self, data=None, password=None):
|
|
||||||
_GNTPBase.__init__(self, 'REGISTER')
|
|
||||||
self.notifications = []
|
|
||||||
|
|
||||||
if data:
|
|
||||||
self.decode(data, password)
|
|
||||||
else:
|
|
||||||
self.set_password(password)
|
|
||||||
self.add_header('Application-Name', 'pygntp')
|
|
||||||
self.add_header('Notifications-Count', 0)
|
|
||||||
|
|
||||||
def validate(self):
|
|
||||||
'''Validate required headers and validate notification headers'''
|
|
||||||
for header in self._requiredHeaders:
|
|
||||||
if not self.headers.get(header, False):
|
|
||||||
raise errors.ParseError('Missing Registration Header: ' + header)
|
|
||||||
for notice in self.notifications:
|
|
||||||
for header in self._requiredNotificationHeaders:
|
|
||||||
if not notice.get(header, False):
|
|
||||||
raise errors.ParseError('Missing Notification Header: ' + header)
|
|
||||||
|
|
||||||
def decode(self, data, password):
|
|
||||||
"""Decode existing GNTP Registration message
|
|
||||||
|
|
||||||
:param string data: Message to decode
|
|
||||||
"""
|
|
||||||
self.raw = gntp.shim.u(data)
|
|
||||||
parts = self.raw.split('\r\n\r\n')
|
|
||||||
self.info = self._parse_info(self.raw)
|
|
||||||
self._validate_password(password)
|
|
||||||
self.headers = self._parse_dict(parts[0])
|
|
||||||
|
|
||||||
for i, part in enumerate(parts):
|
|
||||||
if i == 0:
|
|
||||||
continue # Skip Header
|
|
||||||
if part.strip() == '':
|
|
||||||
continue
|
|
||||||
notice = self._parse_dict(part)
|
|
||||||
if notice.get('Notification-Name', False):
|
|
||||||
self.notifications.append(notice)
|
|
||||||
elif notice.get('Identifier', False):
|
|
||||||
notice['Data'] = self._decode_binary(part, notice)
|
|
||||||
#open('register.png','wblol').write(notice['Data'])
|
|
||||||
self.resources[notice.get('Identifier')] = notice
|
|
||||||
|
|
||||||
def add_notification(self, name, enabled=True):
|
|
||||||
"""Add new Notification to Registration message
|
|
||||||
|
|
||||||
:param string name: Notification Name
|
|
||||||
:param boolean enabled: Enable this notification by default
|
|
||||||
"""
|
|
||||||
notice = {}
|
|
||||||
notice['Notification-Name'] = name
|
|
||||||
notice['Notification-Enabled'] = enabled
|
|
||||||
|
|
||||||
self.notifications.append(notice)
|
|
||||||
self.add_header('Notifications-Count', len(self.notifications))
|
|
||||||
|
|
||||||
def encode(self):
|
|
||||||
"""Encode a GNTP Registration Message
|
|
||||||
|
|
||||||
:return string: Encoded GNTP Registration message. Returned as a byte string
|
|
||||||
"""
|
|
||||||
|
|
||||||
buff = _GNTPBuffer()
|
|
||||||
|
|
||||||
buff.writeln(self._format_info())
|
|
||||||
|
|
||||||
#Headers
|
|
||||||
for k, v in list(self.headers.items()):
|
|
||||||
buff.writeheader(k, v)
|
|
||||||
buff.writeln()
|
|
||||||
|
|
||||||
#Notifications
|
|
||||||
if len(self.notifications) > 0:
|
|
||||||
for notice in self.notifications:
|
|
||||||
for k, v in list(notice.items()):
|
|
||||||
buff.writeheader(k, v)
|
|
||||||
buff.writeln()
|
|
||||||
|
|
||||||
#Resources
|
|
||||||
for resource, data in list(self.resources.items()):
|
|
||||||
buff.writeheader('Identifier', resource)
|
|
||||||
buff.writeheader('Length', len(data))
|
|
||||||
buff.writeln()
|
|
||||||
buff.write(data)
|
|
||||||
buff.writeln()
|
|
||||||
buff.writeln()
|
|
||||||
|
|
||||||
return buff.getvalue()
|
|
||||||
|
|
||||||
|
|
||||||
class GNTPNotice(_GNTPBase):
|
|
||||||
"""Represents a GNTP Notification Command
|
|
||||||
|
|
||||||
:param string data: (Optional) See decode()
|
|
||||||
:param string app: (Optional) Set Application-Name
|
|
||||||
:param string name: (Optional) Set Notification-Name
|
|
||||||
:param string title: (Optional) Set Notification Title
|
|
||||||
:param string password: (Optional) Password to use while encoding/decoding messages
|
|
||||||
"""
|
|
||||||
_requiredHeaders = [
|
|
||||||
'Application-Name',
|
|
||||||
'Notification-Name',
|
|
||||||
'Notification-Title'
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self, data=None, app=None, name=None, title=None, password=None):
|
|
||||||
_GNTPBase.__init__(self, 'NOTIFY')
|
|
||||||
|
|
||||||
if data:
|
|
||||||
self.decode(data, password)
|
|
||||||
else:
|
|
||||||
self.set_password(password)
|
|
||||||
if app:
|
|
||||||
self.add_header('Application-Name', app)
|
|
||||||
if name:
|
|
||||||
self.add_header('Notification-Name', name)
|
|
||||||
if title:
|
|
||||||
self.add_header('Notification-Title', title)
|
|
||||||
|
|
||||||
def decode(self, data, password):
|
|
||||||
"""Decode existing GNTP Notification message
|
|
||||||
|
|
||||||
:param string data: Message to decode.
|
|
||||||
"""
|
|
||||||
self.raw = gntp.shim.u(data)
|
|
||||||
parts = self.raw.split('\r\n\r\n')
|
|
||||||
self.info = self._parse_info(self.raw)
|
|
||||||
self._validate_password(password)
|
|
||||||
self.headers = self._parse_dict(parts[0])
|
|
||||||
|
|
||||||
for i, part in enumerate(parts):
|
|
||||||
if i == 0:
|
|
||||||
continue # Skip Header
|
|
||||||
if part.strip() == '':
|
|
||||||
continue
|
|
||||||
notice = self._parse_dict(part)
|
|
||||||
if notice.get('Identifier', False):
|
|
||||||
notice['Data'] = self._decode_binary(part, notice)
|
|
||||||
#open('notice.png','wblol').write(notice['Data'])
|
|
||||||
self.resources[notice.get('Identifier')] = notice
|
|
||||||
|
|
||||||
|
|
||||||
class GNTPSubscribe(_GNTPBase):
|
|
||||||
"""Represents a GNTP Subscribe Command
|
|
||||||
|
|
||||||
:param string data: (Optional) See decode()
|
|
||||||
:param string password: (Optional) Password to use while encoding/decoding messages
|
|
||||||
"""
|
|
||||||
_requiredHeaders = [
|
|
||||||
'Subscriber-ID',
|
|
||||||
'Subscriber-Name',
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self, data=None, password=None):
|
|
||||||
_GNTPBase.__init__(self, 'SUBSCRIBE')
|
|
||||||
if data:
|
|
||||||
self.decode(data, password)
|
|
||||||
else:
|
|
||||||
self.set_password(password)
|
|
||||||
|
|
||||||
|
|
||||||
class GNTPOK(_GNTPBase):
|
|
||||||
"""Represents a GNTP OK Response
|
|
||||||
|
|
||||||
:param string data: (Optional) See _GNTPResponse.decode()
|
|
||||||
:param string action: (Optional) Set type of action the OK Response is for
|
|
||||||
"""
|
|
||||||
_requiredHeaders = ['Response-Action']
|
|
||||||
|
|
||||||
def __init__(self, data=None, action=None):
|
|
||||||
_GNTPBase.__init__(self, '-OK')
|
|
||||||
if data:
|
|
||||||
self.decode(data)
|
|
||||||
if action:
|
|
||||||
self.add_header('Response-Action', action)
|
|
||||||
|
|
||||||
|
|
||||||
class GNTPError(_GNTPBase):
|
|
||||||
"""Represents a GNTP Error response
|
|
||||||
|
|
||||||
:param string data: (Optional) See _GNTPResponse.decode()
|
|
||||||
:param string errorcode: (Optional) Error code
|
|
||||||
:param string errordesc: (Optional) Error Description
|
|
||||||
"""
|
|
||||||
_requiredHeaders = ['Error-Code', 'Error-Description']
|
|
||||||
|
|
||||||
def __init__(self, data=None, errorcode=None, errordesc=None):
|
|
||||||
_GNTPBase.__init__(self, '-ERROR')
|
|
||||||
if data:
|
|
||||||
self.decode(data)
|
|
||||||
if errorcode:
|
|
||||||
self.add_header('Error-Code', errorcode)
|
|
||||||
self.add_header('Error-Description', errordesc)
|
|
||||||
|
|
||||||
def error(self):
|
|
||||||
return (self.headers.get('Error-Code', None),
|
|
||||||
self.headers.get('Error-Description', None))
|
|
||||||
|
|
||||||
|
|
||||||
def parse_gntp(data, password=None):
|
|
||||||
"""Attempt to parse a message as a GNTP message
|
|
||||||
|
|
||||||
:param string data: Message to be parsed
|
|
||||||
:param string password: Optional password to be used to verify the message
|
|
||||||
"""
|
|
||||||
data = gntp.shim.u(data)
|
|
||||||
match = GNTP_INFO_LINE_SHORT.match(data)
|
|
||||||
if not match:
|
|
||||||
raise errors.ParseError('INVALID_GNTP_INFO')
|
|
||||||
info = match.groupdict()
|
|
||||||
if info['messagetype'] == 'REGISTER':
|
|
||||||
return GNTPRegister(data, password=password)
|
|
||||||
elif info['messagetype'] == 'NOTIFY':
|
|
||||||
return GNTPNotice(data, password=password)
|
|
||||||
elif info['messagetype'] == 'SUBSCRIBE':
|
|
||||||
return GNTPSubscribe(data, password=password)
|
|
||||||
elif info['messagetype'] == '-OK':
|
|
||||||
return GNTPOK(data)
|
|
||||||
elif info['messagetype'] == '-ERROR':
|
|
||||||
return GNTPError(data)
|
|
||||||
raise errors.ParseError('INVALID_GNTP_MESSAGE')
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# Copyright: 2013 Paul Traylor
|
|
||||||
# These sources are released under the terms of the MIT license: see LICENSE
|
|
||||||
|
|
||||||
class BaseError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ParseError(BaseError):
|
|
||||||
errorcode = 500
|
|
||||||
errordesc = 'Error parsing the message'
|
|
||||||
|
|
||||||
|
|
||||||
class AuthError(BaseError):
|
|
||||||
errorcode = 400
|
|
||||||
errordesc = 'Error with authorization'
|
|
||||||
|
|
||||||
|
|
||||||
class UnsupportedError(BaseError):
|
|
||||||
errorcode = 500
|
|
||||||
errordesc = 'Currently unsupported by gntp.py'
|
|
||||||
|
|
||||||
|
|
||||||
class NetworkError(BaseError):
|
|
||||||
errorcode = 500
|
|
||||||
errordesc = "Error connecting to growl server"
|
|
||||||
@@ -1,265 +0,0 @@
|
|||||||
# Copyright: 2013 Paul Traylor
|
|
||||||
# These sources are released under the terms of the MIT license: see LICENSE
|
|
||||||
|
|
||||||
"""
|
|
||||||
The gntp.notifier module is provided as a simple way to send notifications
|
|
||||||
using GNTP
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
This class is intended to mostly mirror the older Python bindings such
|
|
||||||
that you should be able to replace instances of the old bindings with
|
|
||||||
this class.
|
|
||||||
`Original Python bindings <http://code.google.com/p/growl/source/browse/Bindings/python/Growl.py>`_
|
|
||||||
|
|
||||||
"""
|
|
||||||
import logging
|
|
||||||
import platform
|
|
||||||
import socket
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from gntp.version import __version__
|
|
||||||
import gntp.core
|
|
||||||
import gntp.errors as errors
|
|
||||||
import gntp.shim
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'mini',
|
|
||||||
'GrowlNotifier',
|
|
||||||
]
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class GrowlNotifier(object):
|
|
||||||
"""Helper class to simplfy sending Growl messages
|
|
||||||
|
|
||||||
:param string applicationName: Sending application name
|
|
||||||
:param list notification: List of valid notifications
|
|
||||||
:param list defaultNotifications: List of notifications that should be enabled
|
|
||||||
by default
|
|
||||||
:param string applicationIcon: Icon URL
|
|
||||||
:param string hostname: Remote host
|
|
||||||
:param integer port: Remote port
|
|
||||||
"""
|
|
||||||
|
|
||||||
passwordHash = 'MD5'
|
|
||||||
socketTimeout = 3
|
|
||||||
|
|
||||||
def __init__(self, applicationName='Python GNTP', notifications=[],
|
|
||||||
defaultNotifications=None, applicationIcon=None, hostname='localhost',
|
|
||||||
password=None, port=23053):
|
|
||||||
|
|
||||||
self.applicationName = applicationName
|
|
||||||
self.notifications = list(notifications)
|
|
||||||
if defaultNotifications:
|
|
||||||
self.defaultNotifications = list(defaultNotifications)
|
|
||||||
else:
|
|
||||||
self.defaultNotifications = self.notifications
|
|
||||||
self.applicationIcon = applicationIcon
|
|
||||||
|
|
||||||
self.password = password
|
|
||||||
self.hostname = hostname
|
|
||||||
self.port = int(port)
|
|
||||||
|
|
||||||
def _checkIcon(self, data):
|
|
||||||
'''
|
|
||||||
Check the icon to see if it's valid
|
|
||||||
|
|
||||||
If it's a simple URL icon, then we return True. If it's a data icon
|
|
||||||
then we return False
|
|
||||||
'''
|
|
||||||
logger.info('Checking icon')
|
|
||||||
return gntp.shim.u(data).startswith('http')
|
|
||||||
|
|
||||||
def register(self):
|
|
||||||
"""Send GNTP Registration
|
|
||||||
|
|
||||||
.. warning::
|
|
||||||
Before sending notifications to Growl, you need to have
|
|
||||||
sent a registration message at least once
|
|
||||||
"""
|
|
||||||
logger.info('Sending registration to %s:%s', self.hostname, self.port)
|
|
||||||
register = gntp.core.GNTPRegister()
|
|
||||||
register.add_header('Application-Name', self.applicationName)
|
|
||||||
for notification in self.notifications:
|
|
||||||
enabled = notification in self.defaultNotifications
|
|
||||||
register.add_notification(notification, enabled)
|
|
||||||
if self.applicationIcon:
|
|
||||||
if self._checkIcon(self.applicationIcon):
|
|
||||||
register.add_header('Application-Icon', self.applicationIcon)
|
|
||||||
else:
|
|
||||||
resource = register.add_resource(self.applicationIcon)
|
|
||||||
register.add_header('Application-Icon', resource)
|
|
||||||
if self.password:
|
|
||||||
register.set_password(self.password, self.passwordHash)
|
|
||||||
self.add_origin_info(register)
|
|
||||||
self.register_hook(register)
|
|
||||||
return self._send('register', register)
|
|
||||||
|
|
||||||
def notify(self, noteType, title, description, icon=None, sticky=False,
|
|
||||||
priority=None, callback=None, identifier=None, custom={}):
|
|
||||||
"""Send a GNTP notifications
|
|
||||||
|
|
||||||
.. warning::
|
|
||||||
Must have registered with growl beforehand or messages will be ignored
|
|
||||||
|
|
||||||
:param string noteType: One of the notification names registered earlier
|
|
||||||
:param string title: Notification title (usually displayed on the notification)
|
|
||||||
:param string description: The main content of the notification
|
|
||||||
:param string icon: Icon URL path
|
|
||||||
:param boolean sticky: Sticky notification
|
|
||||||
:param integer priority: Message priority level from -2 to 2
|
|
||||||
:param string callback: URL callback
|
|
||||||
:param dict custom: Custom attributes. Key names should be prefixed with X-
|
|
||||||
according to the spec but this is not enforced by this class
|
|
||||||
|
|
||||||
.. warning::
|
|
||||||
For now, only URL callbacks are supported. In the future, the
|
|
||||||
callback argument will also support a function
|
|
||||||
"""
|
|
||||||
logger.info('Sending notification [%s] to %s:%s', noteType, self.hostname, self.port)
|
|
||||||
assert noteType in self.notifications
|
|
||||||
notice = gntp.core.GNTPNotice()
|
|
||||||
notice.add_header('Application-Name', self.applicationName)
|
|
||||||
notice.add_header('Notification-Name', noteType)
|
|
||||||
notice.add_header('Notification-Title', title)
|
|
||||||
if self.password:
|
|
||||||
notice.set_password(self.password, self.passwordHash)
|
|
||||||
if sticky:
|
|
||||||
notice.add_header('Notification-Sticky', sticky)
|
|
||||||
if priority:
|
|
||||||
notice.add_header('Notification-Priority', priority)
|
|
||||||
if icon:
|
|
||||||
if self._checkIcon(icon):
|
|
||||||
notice.add_header('Notification-Icon', icon)
|
|
||||||
else:
|
|
||||||
resource = notice.add_resource(icon)
|
|
||||||
notice.add_header('Notification-Icon', resource)
|
|
||||||
|
|
||||||
if description:
|
|
||||||
notice.add_header('Notification-Text', description)
|
|
||||||
if callback:
|
|
||||||
notice.add_header('Notification-Callback-Target', callback)
|
|
||||||
if identifier:
|
|
||||||
notice.add_header('Notification-Coalescing-ID', identifier)
|
|
||||||
|
|
||||||
for key in custom:
|
|
||||||
notice.add_header(key, custom[key])
|
|
||||||
|
|
||||||
self.add_origin_info(notice)
|
|
||||||
self.notify_hook(notice)
|
|
||||||
|
|
||||||
return self._send('notify', notice)
|
|
||||||
|
|
||||||
def subscribe(self, id, name, port):
|
|
||||||
"""Send a Subscribe request to a remote machine"""
|
|
||||||
sub = gntp.core.GNTPSubscribe()
|
|
||||||
sub.add_header('Subscriber-ID', id)
|
|
||||||
sub.add_header('Subscriber-Name', name)
|
|
||||||
sub.add_header('Subscriber-Port', port)
|
|
||||||
if self.password:
|
|
||||||
sub.set_password(self.password, self.passwordHash)
|
|
||||||
|
|
||||||
self.add_origin_info(sub)
|
|
||||||
self.subscribe_hook(sub)
|
|
||||||
|
|
||||||
return self._send('subscribe', sub)
|
|
||||||
|
|
||||||
def add_origin_info(self, packet):
|
|
||||||
"""Add optional Origin headers to message"""
|
|
||||||
packet.add_header('Origin-Machine-Name', platform.node())
|
|
||||||
packet.add_header('Origin-Software-Name', 'gntp.py')
|
|
||||||
packet.add_header('Origin-Software-Version', __version__)
|
|
||||||
packet.add_header('Origin-Platform-Name', platform.system())
|
|
||||||
packet.add_header('Origin-Platform-Version', platform.platform())
|
|
||||||
|
|
||||||
def register_hook(self, packet):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def notify_hook(self, packet):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def subscribe_hook(self, packet):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _send(self, messagetype, packet):
|
|
||||||
"""Send the GNTP Packet"""
|
|
||||||
|
|
||||||
packet.validate()
|
|
||||||
data = packet.encode()
|
|
||||||
|
|
||||||
logger.debug('To : %s:%s <%s>\n%s', self.hostname, self.port, packet.__class__, data)
|
|
||||||
|
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
s.settimeout(self.socketTimeout)
|
|
||||||
try:
|
|
||||||
s.connect((self.hostname, self.port))
|
|
||||||
s.send(data)
|
|
||||||
recv_data = s.recv(1024)
|
|
||||||
while not recv_data.endswith(gntp.shim.b("\r\n\r\n")):
|
|
||||||
recv_data += s.recv(1024)
|
|
||||||
except socket.error:
|
|
||||||
# Python2.5 and Python3 compatibile exception
|
|
||||||
exc = sys.exc_info()[1]
|
|
||||||
raise errors.NetworkError(exc)
|
|
||||||
|
|
||||||
response = gntp.core.parse_gntp(recv_data)
|
|
||||||
s.close()
|
|
||||||
|
|
||||||
logger.debug('From : %s:%s <%s>\n%s', self.hostname, self.port, response.__class__, response)
|
|
||||||
|
|
||||||
if type(response) == gntp.core.GNTPOK:
|
|
||||||
return True
|
|
||||||
logger.error('Invalid response: %s', response.error())
|
|
||||||
return response.error()
|
|
||||||
|
|
||||||
|
|
||||||
def mini(description, applicationName='PythonMini', noteType="Message",
|
|
||||||
title="Mini Message", applicationIcon=None, hostname='localhost',
|
|
||||||
password=None, port=23053, sticky=False, priority=None,
|
|
||||||
callback=None, notificationIcon=None, identifier=None,
|
|
||||||
notifierFactory=GrowlNotifier):
|
|
||||||
"""Single notification function
|
|
||||||
|
|
||||||
Simple notification function in one line. Has only one required parameter
|
|
||||||
and attempts to use reasonable defaults for everything else
|
|
||||||
:param string description: Notification message
|
|
||||||
|
|
||||||
.. warning::
|
|
||||||
For now, only URL callbacks are supported. In the future, the
|
|
||||||
callback argument will also support a function
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
growl = notifierFactory(
|
|
||||||
applicationName=applicationName,
|
|
||||||
notifications=[noteType],
|
|
||||||
defaultNotifications=[noteType],
|
|
||||||
applicationIcon=applicationIcon,
|
|
||||||
hostname=hostname,
|
|
||||||
password=password,
|
|
||||||
port=port,
|
|
||||||
)
|
|
||||||
result = growl.register()
|
|
||||||
if result is not True:
|
|
||||||
return result
|
|
||||||
|
|
||||||
return growl.notify(
|
|
||||||
noteType=noteType,
|
|
||||||
title=title,
|
|
||||||
description=description,
|
|
||||||
icon=notificationIcon,
|
|
||||||
sticky=sticky,
|
|
||||||
priority=priority,
|
|
||||||
callback=callback,
|
|
||||||
identifier=identifier,
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
# We want the "mini" function to be simple and swallow Exceptions
|
|
||||||
# in order to be less invasive
|
|
||||||
logger.exception("Growl error")
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
# If we're running this module directly we're likely running it as a test
|
|
||||||
# so extra debugging is useful
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
|
||||||
mini('Testing mini notification')
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
# Copyright: 2013 Paul Traylor
|
|
||||||
# These sources are released under the terms of the MIT license: see LICENSE
|
|
||||||
|
|
||||||
"""
|
|
||||||
Python2.5 and Python3.3 compatibility shim
|
|
||||||
|
|
||||||
Heavily inspirted by the "six" library.
|
|
||||||
https://pypi.python.org/pypi/six
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
PY3 = sys.version_info[0] == 3
|
|
||||||
|
|
||||||
if PY3:
|
|
||||||
def b(s):
|
|
||||||
if isinstance(s, bytes):
|
|
||||||
return s
|
|
||||||
return s.encode('utf8', 'replace')
|
|
||||||
|
|
||||||
def u(s):
|
|
||||||
if isinstance(s, bytes):
|
|
||||||
return s.decode('utf8', 'replace')
|
|
||||||
return s
|
|
||||||
|
|
||||||
from io import BytesIO as StringIO
|
|
||||||
from configparser import RawConfigParser
|
|
||||||
else:
|
|
||||||
def b(s):
|
|
||||||
if isinstance(s, str):
|
|
||||||
return s.encode('utf8', 'replace')
|
|
||||||
return s
|
|
||||||
|
|
||||||
def u(s):
|
|
||||||
if isinstance(s, str):
|
|
||||||
return s
|
|
||||||
if isinstance(s, int):
|
|
||||||
s = str(s)
|
|
||||||
return str(s, "utf8", "replace")
|
|
||||||
|
|
||||||
from io import StringIO
|
|
||||||
from configparser import RawConfigParser
|
|
||||||
|
|
||||||
b.__doc__ = "Ensure we have a byte string"
|
|
||||||
u.__doc__ = "Ensure we have a unicode string"
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
# Copyright: 2013 Paul Traylor
|
|
||||||
# These sources are released under the terms of the MIT license: see LICENSE
|
|
||||||
|
|
||||||
__version__ = '1.0.2'
|
|
||||||
Binary file not shown.
@@ -1,133 +0,0 @@
|
|||||||
#!/usr/bin/python
|
|
||||||
|
|
||||||
import shutil
|
|
||||||
import os
|
|
||||||
import stat
|
|
||||||
import platform
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
def registerapp(app):
|
|
||||||
|
|
||||||
# don't do any of this unless >= 10.8
|
|
||||||
if not [int(n) for n in platform.mac_ver()[0].split('.')] >= [10, 8]:
|
|
||||||
return None, 'Registering requires OS X version >= 10.8'
|
|
||||||
|
|
||||||
app_path = None
|
|
||||||
|
|
||||||
# check app bundle doesn't already exist
|
|
||||||
app_path = subprocess.check_output(['/usr/bin/mdfind', 'kMDItemCFBundleIdentifier == "ade.headphones.osxnotify"']).strip()
|
|
||||||
if app_path:
|
|
||||||
return app_path, 'App previously registered'
|
|
||||||
|
|
||||||
# check app doesn't already exist
|
|
||||||
app = app.strip()
|
|
||||||
if not app:
|
|
||||||
return None, 'Path/Application not entered'
|
|
||||||
if os.path.splitext(app)[1] == ".app":
|
|
||||||
app_path = app
|
|
||||||
else:
|
|
||||||
app_path = app + '.app'
|
|
||||||
if os.path.exists(app_path):
|
|
||||||
return None, 'App %s already exists, choose a different name' % app_path
|
|
||||||
|
|
||||||
# generate app
|
|
||||||
try:
|
|
||||||
os.mkdir(app_path)
|
|
||||||
os.mkdir(app_path + "/Contents")
|
|
||||||
os.mkdir(app_path + "/Contents/MacOS")
|
|
||||||
os.mkdir(app_path + "/Contents/Resources")
|
|
||||||
shutil.copy(os.path.join(os.path.dirname(__file__), "appIcon.icns"), app_path + "/Contents/Resources/")
|
|
||||||
|
|
||||||
version = "1.0.0"
|
|
||||||
bundleName = "OSXNotify"
|
|
||||||
bundleIdentifier = "ade.headphones.osxnotify"
|
|
||||||
|
|
||||||
f = open(app_path + "/Contents/Info.plist", "w")
|
|
||||||
f.write("""<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
|
||||||
<string>English</string>
|
|
||||||
<key>CFBundleExecutable</key>
|
|
||||||
<string>main.py</string>
|
|
||||||
<key>CFBundleGetInfoString</key>
|
|
||||||
<string>%s</string>
|
|
||||||
<key>CFBundleIconFile</key>
|
|
||||||
<string>appIcon.icns</string>
|
|
||||||
<key>CFBundleIdentifier</key>
|
|
||||||
<string>%s</string>
|
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
|
||||||
<string>6.0</string>
|
|
||||||
<key>CFBundleName</key>
|
|
||||||
<string>%s</string>
|
|
||||||
<key>CFBundlePackageType</key>
|
|
||||||
<string>APPL</string>
|
|
||||||
<key>CFBundleShortVersionString</key>
|
|
||||||
<string>%s</string>
|
|
||||||
<key>CFBundleSignature</key>
|
|
||||||
<string>????</string>
|
|
||||||
<key>CFBundleVersion</key>
|
|
||||||
<string>%s</string>
|
|
||||||
<key>NSAppleScriptEnabled</key>
|
|
||||||
<string>YES</string>
|
|
||||||
<key>NSMainNibFile</key>
|
|
||||||
<string>MainMenu</string>
|
|
||||||
<key>NSPrincipalClass</key>
|
|
||||||
<string>NSApplication</string>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
""" % (bundleName + " " + version, bundleIdentifier, bundleName, bundleName + " " + version, version))
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
f = open(app_path + "/Contents/PkgInfo", "w")
|
|
||||||
f.write("APPL????")
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
f = open(app_path + "/Contents/MacOS/main.py", "w")
|
|
||||||
f.write("""#!/usr/bin/python
|
|
||||||
|
|
||||||
objc = None
|
|
||||||
|
|
||||||
def swizzle(cls, SEL, func):
|
|
||||||
old_IMP = cls.instanceMethodForSelector_(SEL)
|
|
||||||
def wrapper(self, *args, **kwargs):
|
|
||||||
return func(self, old_IMP, *args, **kwargs)
|
|
||||||
new_IMP = objc.selector(wrapper, selector=old_IMP.selector,
|
|
||||||
signature=old_IMP.signature)
|
|
||||||
objc.classAddMethod(cls, SEL, new_IMP)
|
|
||||||
|
|
||||||
def notify(title, subtitle=None, text=None, sound=True):
|
|
||||||
global objc
|
|
||||||
objc = __import__("objc")
|
|
||||||
swizzle(objc.lookUpClass('NSBundle'),
|
|
||||||
b'bundleIdentifier',
|
|
||||||
swizzled_bundleIdentifier)
|
|
||||||
NSUserNotification = objc.lookUpClass('NSUserNotification')
|
|
||||||
NSUserNotificationCenter = objc.lookUpClass('NSUserNotificationCenter')
|
|
||||||
NSAutoreleasePool = objc.lookUpClass('NSAutoreleasePool')
|
|
||||||
pool = NSAutoreleasePool.alloc().init()
|
|
||||||
notification = NSUserNotification.alloc().init()
|
|
||||||
notification.setTitle_(title)
|
|
||||||
notification.setSubtitle_(subtitle)
|
|
||||||
notification.setInformativeText_(text)
|
|
||||||
notification.setSoundName_("NSUserNotificationDefaultSoundName")
|
|
||||||
notification_center = NSUserNotificationCenter.defaultUserNotificationCenter()
|
|
||||||
notification_center.deliverNotification_(notification)
|
|
||||||
del pool
|
|
||||||
|
|
||||||
def swizzled_bundleIdentifier(self, original):
|
|
||||||
return 'ade.headphones.osxnotify'
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
notify('Half Man Half Biscuit', 'Back in the DHSS', '99% Of Gargoyles Look Like Bob Todd')
|
|
||||||
""")
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
oldmode = os.stat(app_path + "/Contents/MacOS/main.py").st_mode
|
|
||||||
os.chmod(app_path + "/Contents/MacOS/main.py", oldmode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
||||||
|
|
||||||
return app_path, 'App registered'
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return None, 'Error creating App %s. %s' % (app_path, e)
|
|
||||||
Reference in New Issue
Block a user