mirror of
https://github.com/riwiwa/muzi.git
synced 2026-03-04 00:51:59 -08:00
add scrobble removal
This commit is contained in:
@@ -794,6 +794,7 @@ func GetHistoryForSong(userId, songId int, limit, offset int) ([]ScrobbleEntry,
|
||||
}
|
||||
|
||||
type ScrobbleEntry struct {
|
||||
Id int
|
||||
Timestamp time.Time
|
||||
SongName string
|
||||
ArtistName string
|
||||
@@ -958,7 +959,7 @@ func GetHistoryForSongs(userId int, songIds []int, limit, offset int) ([]Scrobbl
|
||||
return []ScrobbleEntry{}, nil
|
||||
}
|
||||
rows, err := Pool.Query(context.Background(),
|
||||
`SELECT h.timestamp, h.song_name, h.album_name, h.ms_played, h.platform,
|
||||
`SELECT h.id, h.timestamp, h.song_name, h.album_name, h.ms_played, h.platform,
|
||||
(SELECT name FROM artists WHERE id = h.artist_id) as artist_name,
|
||||
h.artist_ids
|
||||
FROM history h WHERE h.user_id = $1 AND h.song_id = ANY($2)
|
||||
@@ -972,7 +973,7 @@ func GetHistoryForSongs(userId int, songIds []int, limit, offset int) ([]Scrobbl
|
||||
var entries []ScrobbleEntry
|
||||
for rows.Next() {
|
||||
var e ScrobbleEntry
|
||||
err := rows.Scan(&e.Timestamp, &e.SongName, &e.AlbumName, &e.MsPlayed, &e.Platform, &e.ArtistName, &e.ArtistIds)
|
||||
err := rows.Scan(&e.Id, &e.Timestamp, &e.SongName, &e.AlbumName, &e.MsPlayed, &e.Platform, &e.ArtistName, &e.ArtistIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -980,3 +981,17 @@ func GetHistoryForSongs(userId int, songIds []int, limit, offset int) ([]Scrobbl
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func DeleteHistoryByIds(userId int, ids []int) error {
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
_, err := Pool.Exec(context.Background(),
|
||||
`DELETE FROM history WHERE user_id = $1 AND id = ANY($2)`,
|
||||
userId, ids)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error deleting history: %v\n", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
{{.Song.Title}}
|
||||
{{if eq .LoggedInUsername .Username}}
|
||||
<button class="edit-btn" onclick="openEditModal()">Edit</button>
|
||||
<button class="edit-btn" id="removeScrobblesBtn" onclick="toggleRemoveMode()">Remove</button>
|
||||
{{end}}
|
||||
</h1>
|
||||
{{if .ArtistNames}}
|
||||
@@ -23,9 +24,16 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="history">
|
||||
<h3>Scrobbles</h3>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||
<h3>Scrobbles</h3>
|
||||
<div id="removeControls" style="display: none;">
|
||||
<button class="edit-btn" onclick="cancelRemoveMode()">Cancel</button>
|
||||
<button class="edit-btn" onclick="deleteSelectedScrobbles()" style="background: #c44;">Delete Selected</button>
|
||||
</div>
|
||||
</div>
|
||||
<table>
|
||||
<tr>
|
||||
<th class="remove-checkbox-col" style="display: none;"><input type="checkbox" id="selectAllCheckboxes"></th>
|
||||
<th>Artist</th>
|
||||
<th>Title</th>
|
||||
<th>Album</th>
|
||||
@@ -34,6 +42,9 @@
|
||||
{{$username := .Username}}
|
||||
{{range .Times}}
|
||||
<tr>
|
||||
<td class="remove-checkbox-col" style="display: none;">
|
||||
<input type="checkbox" class="scrobble-checkbox" value="{{.Id}}">
|
||||
</td>
|
||||
<td>
|
||||
{{- $artistNames := getArtistNames .ArtistIds}}
|
||||
{{- range $i, $name := $artistNames}}{{if $i}}, {{end}}<a href="/profile/{{$username}}/artist/{{urlquery $name}}">{{$name}}</a>{{end}}
|
||||
@@ -68,4 +79,67 @@
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<script>
|
||||
function toggleRemoveMode() {
|
||||
var checkboxes = document.querySelectorAll('.remove-checkbox-col');
|
||||
checkboxes.forEach(function(col) {
|
||||
col.style.display = '';
|
||||
});
|
||||
document.getElementById('removeScrobblesBtn').style.display = 'none';
|
||||
document.getElementById('removeControls').style.display = '';
|
||||
|
||||
var selectAll = document.getElementById('selectAllCheckboxes');
|
||||
selectAll.addEventListener('change', function() {
|
||||
document.querySelectorAll('.scrobble-checkbox').forEach(function(cb) {
|
||||
cb.checked = selectAll.checked;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function cancelRemoveMode() {
|
||||
var checkboxes = document.querySelectorAll('.remove-checkbox-col');
|
||||
checkboxes.forEach(function(col) {
|
||||
col.style.display = 'none';
|
||||
});
|
||||
document.getElementById('removeScrobblesBtn').style.display = '';
|
||||
document.getElementById('removeControls').style.display = 'none';
|
||||
|
||||
document.querySelectorAll('.scrobble-checkbox').forEach(function(cb) {
|
||||
cb.checked = false;
|
||||
});
|
||||
document.getElementById('selectAllCheckboxes').checked = false;
|
||||
}
|
||||
|
||||
function deleteSelectedScrobbles() {
|
||||
var checkboxes = document.querySelectorAll('.scrobble-checkbox:checked');
|
||||
var ids = [];
|
||||
checkboxes.forEach(function(cb) {
|
||||
ids.push(parseInt(cb.value));
|
||||
});
|
||||
|
||||
if (ids.length === 0) {
|
||||
alert('Please select at least one scrobble to delete.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm('Are you sure you want to delete ' + ids.length + ' scrobble(s)?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '/api/scrobble/delete', true);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState === 4) {
|
||||
if (xhr.status === 200) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error deleting scrobbles: ' + xhr.responseText);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.send(JSON.stringify(ids));
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
@@ -1002,3 +1002,36 @@ func imageUploadHandler() http.HandlerFunc {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func deleteScrobbleHandler() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
username := getLoggedInUsername(r)
|
||||
if username == "" {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
userId, err := getUserIdByUsername(r.Context(), username)
|
||||
if err != nil {
|
||||
http.Error(w, "User not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var ids []int
|
||||
if err := json.NewDecoder(r.Body).Decode(&ids); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error decoding request: %v\n", err)
|
||||
http.Error(w, "Invalid request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = db.DeleteHistoryByIds(userId, ids)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error deleting scrobbles: %v\n", err)
|
||||
http.Error(w, "Error deleting scrobbles", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +120,7 @@ func Start() {
|
||||
r.Patch("/api/artist/{id}/batch", artistBatchEditHandler())
|
||||
r.Patch("/api/song/{id}/batch", songBatchEditHandler())
|
||||
r.Patch("/api/album/{id}/batch", albumBatchEditHandler())
|
||||
r.Post("/api/scrobble/delete", deleteScrobbleHandler())
|
||||
r.Post("/api/upload/image", imageUploadHandler())
|
||||
r.Get("/search", searchHandler())
|
||||
r.Get("/import", importPageHandler())
|
||||
|
||||
Reference in New Issue
Block a user